telegramTextOnly = "Only text messages are supported."
)
+// TelegramConfig holds the settings of telegram.json: the bot token and the
+// user ID of the owner. The token is a secret, so the file must have mode 0600.
+type TelegramConfig struct {
+ Token string `json:"token"`
+ Owner int64 `json:"owner"`
+}
+
+// TelegramBot connects the private chat of the owner with the agent. It polls
+// the Bot API for new messages and sends owner text, images, and voice notes to
+// the agent. It ignores every other update. Its sender sends the replies, the
+// errors, the notices of the bot, and the user messages of the other clients to
+// the chat, and shows the typing action while a turn runs. Only the replies
+// have a format: their Markdown goes as rich messages, which Telegram renders
+// itself, and as message entities only when Telegram refuses a rich message.
+// The rest is plain text. Tool calls do not go to the chat.
+//
+// The offset file holds the ID of the next update. It moves past an update only
+// after the message of the update is durable in the log, so a restart does not
+// lose a message. A crash between the flush and the offset write makes Telegram
+// send the update again, and the message repeats once.
+//
+// A failed Bot API call waits and tries again, from telegramRetryDelay up to
+// telegramRetryLimit, or as long as the Bot API asks. It never stops the agent.
+// A TelegramOutage logs the first failure and the end of each run of failures,
+// and the retries between them log nothing.
+type TelegramBot struct {
+ api *TelegramAPI
+ voice *VoiceTranscriber
+ owner int64
+ offsetPath string
+ logger *slog.Logger
+
+ mutex sync.Mutex
+ queue []Event
+ ready chan struct{}
+}
+
+// TelegramOutage is one run of failures of a kind of Bot API call. The first
+// failure logs loss, its cause, and the wait of the next try. The retries log
+// nothing. The next answer of Telegram logs back and the length of the outage.
+// loss and back say what the user loses and gets again.
+type TelegramOutage struct {
+ logger *slog.Logger
+ loss string
+ back string
+ start time.Time
+}
+
+// TelegramPart is the text of one message to the chat, at most
+// telegramMessageUnits long, and its entities.
+type TelegramPart struct {
+ text string
+ entities []TelegramEntity
+}
+
+// TelegramOffsetFile is the content of the offset file.
+type TelegramOffsetFile struct {
+ Offset int64 `json:"offset"`
+}
+
// loadTelegramConfig reads telegram.json in dir. It returns false when the file
// does not exist. An error never holds the token.
func loadTelegramConfig(dir string) (TelegramConfig, bool, error) {
}
}
-// TelegramConfig holds the settings of telegram.json: the bot token and the
-// user ID of the owner. The token is a secret, so the file must have mode 0600.
-type TelegramConfig struct {
- Token string `json:"token"`
- Owner int64 `json:"owner"`
-}
-
// validate makes sure that the token has the form of a bot token and that the
// owner is a user ID. The token goes into a URL path, so it can only hold the
// characters of a bot token.
return nil
}
-// TelegramBot connects the private chat of the owner with the agent. It polls
-// the Bot API for new messages and sends owner text and images to the agent. It
-// ignores every other update. Its sender sends the replies, the errors, the
-// notices of the bot, and the user messages of the other clients to the chat,
-// and shows the typing action while a turn runs. Only the replies have a
-// format: their Markdown goes as rich messages, which Telegram renders itself,
-// and as message entities only when Telegram refuses a rich message. The rest
-// is plain text. Tool calls do not go to the chat.
-//
-// The offset file holds the ID of the next update. It moves past an update
-// only after the message of the update is durable in the log, so a restart
-// does not lose a message. A crash between the flush and the offset write
-// makes Telegram send the update again, and the message repeats once.
-//
-// A failed Bot API call waits and tries again, from telegramRetryDelay up to
-// telegramRetryLimit, or as long as the Bot API asks. It never stops the agent.
-// A TelegramOutage logs the first failure and the end of each run of failures,
-// and the retries between them log nothing.
-type TelegramBot struct {
- api *TelegramAPI
- voice *VoiceTranscriber
- owner int64
- offsetPath string
- logger *slog.Logger
-
- mutex sync.Mutex
- queue []Event
- ready chan struct{}
-}
-
-// Notify queues event for the chat. It never blocks, so a slow network does not
-// stop the agent. The WhatsApp batches and the runs of the schedules do not go
-// to the chat: the reply of the agent tells the user what needs the user.
-func (b *TelegramBot) Notify(event Event) {
- switch event.Kind {
- case "busy", "idle", "reply", "error", "notice", "subagent":
- case "work":
- if event.Source == whatsappSource || event.Source == scheduleSource {
- return
- }
- case "user":
- if event.Source == telegramSource {
- return
- }
- default:
- return
- }
-
- b.mutex.Lock()
-
- b.queue = append(b.queue, event)
- b.mutex.Unlock()
-
- select {
- case b.ready <- struct{}{}:
- default:
- }
-}
-
// Run polls for the messages of the owner and sends the queued events to the
// chat until ctx ends. It returns an error only when a message or the offset or
// image cannot be written, because then the bot would lose or repeat messages.
func (b *TelegramBot) poll(ctx context.Context, l *TurnLoop, whatsapp *WhatsApp) error {
offset, err := telegramLoadOffset(b.offsetPath)
if err != nil {
- return err
+ return fmt.Errorf("load Telegram offset: %w", err)
}
outage := &TelegramOutage{
continue poll
}
- return err
+ if err != nil {
+ return fmt.Errorf("handle Telegram update: %w", err)
+ }
+
+ return nil
}
imageOutage.End()
offset = update.UpdateID + 1
err = telegramSaveOffset(b.offsetPath, offset)
if err != nil {
- return err
+ return fmt.Errorf("save Telegram offset: %w", err)
}
}
}
// typing shows the typing action once. A failure is not logged, because the
// next message or poll logs the state of the network.
func (b *TelegramBot) typing(ctx context.Context) {
- _ = b.api.SendTyping(ctx, b.owner)
+ // The next message or poll reports a network failure.
+ b.api.SendTyping(ctx, b.owner) // nolint: errcheck
}
// post sends event to the chat. A reply goes as rich messages of its Markdown,
default:
_, err := l.Submit(telegramSource, message.Text)
if err != nil {
- return err
+ return fmt.Errorf("submit Telegram text: %w", err)
}
err = l.Flush(context.WithoutCancel(ctx))
}
}
-// sendOutage returns a new outage of the messages to the chat.
-func (b *TelegramBot) sendOutage() *TelegramOutage {
- return &TelegramOutage{
- logger: b.logger,
- loss: "send failed",
- back: "send back",
- }
-}
-
// notice queues a short answer of the bot for the chat.
func (b *TelegramBot) notice(text string) {
b.Notify(Event{
})
}
-// TelegramOutage is one run of failures of a kind of Bot API call. The first
-// failure logs loss, its cause, and the wait of the next try. The retries log
-// nothing. The next answer of Telegram logs back and the length of the outage.
-// loss and back say what the user loses and gets again.
-type TelegramOutage struct {
- logger *slog.Logger
- loss string
- back string
- start time.Time
+// sendOutage returns a new outage of the messages to the chat.
+func (b *TelegramBot) sendOutage() *TelegramOutage {
+ return &TelegramOutage{
+ logger: b.logger,
+ loss: "send failed",
+ back: "send back",
+ }
}
-// Fail logs the start of an outage with err and delay, unless an outage runs.
-func (o *TelegramOutage) Fail(err error, delay time.Duration) {
- if !o.start.IsZero() {
+// Notify queues event for the chat. It never blocks, so a slow network does not
+// stop the agent. The WhatsApp batches and the runs of the schedules do not go
+// to the chat: the reply of the agent tells the user what needs the user.
+func (b *TelegramBot) Notify(event Event) {
+ switch event.Kind {
+ case "busy", "idle", "reply", "error", "notice", "subagent":
+ case "work":
+ if event.Source == whatsappSource || event.Source == scheduleSource {
+ return
+ }
+ case "user":
+ if event.Source == telegramSource {
+ return
+ }
+ default:
return
}
- o.start = time.Now()
- o.logger.Warn(o.loss, "err", err, "retry", delay)
+ b.mutex.Lock()
+
+ b.queue = append(b.queue, event)
+ b.mutex.Unlock()
+
+ select {
+ case b.ready <- struct{}{}:
+ default:
+ }
}
// End logs the end of the outage that runs, with its length.
o.start = time.Time{}
}
-// TelegramPart is the text of one message to the chat, at most
-// telegramMessageUnits long, and its entities.
-type TelegramPart struct {
- text string
- entities []TelegramEntity
-}
+// Fail logs the start of an outage with err and delay, unless an outage runs.
+func (o *TelegramOutage) Fail(err error, delay time.Duration) {
+ if !o.start.IsZero() {
+ return
+ }
-// TelegramOffsetFile is the content of the offset file.
-type TelegramOffsetFile struct {
- Offset int64 `json:"offset"`
+ o.start = time.Now()
+ o.logger.Warn(o.loss, "err", err, "retry", delay)
}
// telegramLoadOffset reads the update offset at path. A missing file gives 0,