X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/7088b82414bdf947a6f88918c0a7b2c7fb0c53ef..HEAD:/telegram.go?ds=inline diff --git a/telegram.go b/telegram.go index af1b91e..f2c687a 100644 --- a/telegram.go +++ b/telegram.go @@ -7,7 +7,7 @@ import ( "errors" "fmt" "io" - "log" + "log/slog" "net/http" "os" "path/filepath" @@ -73,8 +73,10 @@ func loadTelegramConfig(dir string) (TelegramConfig, bool, error) { } var config TelegramConfig + decoder := json.NewDecoder(bytes.NewReader(data)) decoder.DisallowUnknownFields() + err = decoder.Decode(&config) if err == nil && !errors.Is(decoder.Decode(&struct{}{}), io.EOF) { err = errors.New("data after the JSON object") @@ -95,7 +97,7 @@ func loadTelegramConfig(dir string) (TelegramConfig, bool, error) { // newTelegramBot returns the bot of owner on api. The offset file is at // offsetPath, and logger gets the failures. No log line holds the text of a // message. -func newTelegramBot(api *TelegramAPI, owner int64, offsetPath string, logger *log.Logger) *TelegramBot { +func newTelegramBot(api *TelegramAPI, owner int64, offsetPath string, logger *slog.Logger) *TelegramBot { return &TelegramBot{ api: api, owner: owner, @@ -152,7 +154,7 @@ type TelegramBot struct { api *TelegramAPI owner int64 offsetPath string - logger *log.Logger + logger *slog.Logger minWait time.Duration maxWait time.Duration @@ -180,6 +182,7 @@ func (b *TelegramBot) Notify(event Event) { } b.mutex.Lock() + b.queue = append(b.queue, event) b.mutex.Unlock() @@ -197,7 +200,9 @@ func (b *TelegramBot) Run(ctx context.Context, l *TurnLoop, whatsapp *WhatsApp) defer cancel() var sender sync.WaitGroup + sender.Add(1) + go func() { defer sender.Done() @@ -205,6 +210,7 @@ func (b *TelegramBot) Run(ctx context.Context, l *TurnLoop, whatsapp *WhatsApp) }() err := b.poll(ctx, l, whatsapp) + cancel() sender.Wait() @@ -237,6 +243,7 @@ func (b *TelegramBot) send(ctx context.Context) { switch event.Kind { case "busy": busy = true + b.typing(ctx) ticker.Reset(telegramTypingInterval) case "idle": @@ -281,6 +288,7 @@ func (b *TelegramBot) poll(ctx context.Context, l *TurnLoop, whatsapp *WhatsApp) } outage.End() + wait = b.minWait for _, update := range updates { if ctx.Err() != nil { @@ -371,7 +379,8 @@ func (b *TelegramBot) pause(ctx context.Context, outage *TelegramOutage, err err func (b *TelegramBot) handle(ctx context.Context, l *TurnLoop, whatsapp *WhatsApp, update TelegramUpdate) error { message := update.Message if message == nil || message.Chat.Type != "private" || message.Chat.ID != b.owner || message.From == nil || message.From.ID != b.owner { - b.logger.Print("message ignored: not from the owner") + b.logger.Info("message ignored: not from the owner") + return nil } @@ -429,15 +438,18 @@ func (b *TelegramBot) deliver(ctx context.Context, part TelegramPart) { } var answer *TelegramError + if len(part.entities) > 0 && errors.As(err, &answer) && answer.Status == http.StatusBadRequest { - b.logger.Printf("reply sent as plain text: %v", err) + b.logger.Warn("reply sent as plain text", "err", err) + part.entities = nil continue } if !retry { - b.logger.Printf("reply lost: %v", err) + b.logger.Error("reply lost", "err", err) + return } @@ -467,7 +479,8 @@ func (b *TelegramBot) deliverRich(ctx context.Context, markdown string) { } if telegramRejected(err) { - b.logger.Printf("reply sent with simple formatting: %v", err) + b.logger.Warn("reply sent with simple formatting", "err", err) + for _, part := range telegramReplyParts(telegramUnescapeTags(markdown)) { b.deliver(ctx, part) } @@ -476,7 +489,8 @@ func (b *TelegramBot) deliverRich(ctx context.Context, markdown string) { } if !retry { - b.logger.Printf("reply lost: %v", err) + b.logger.Error("reply lost", "err", err) + return } @@ -506,7 +520,7 @@ func (b *TelegramBot) notice(text string) { // 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 *log.Logger + logger *slog.Logger loss string back string start time.Time @@ -519,7 +533,7 @@ func (o *TelegramOutage) Fail(err error, delay time.Duration) { } o.start = time.Now() - o.logger.Printf("%s: %v (retry in %s)", o.loss, err, durationText(delay)) + o.logger.Warn(o.loss, "err", err, "retry", delay) } // End logs the end of the outage that runs, with its length. @@ -528,7 +542,8 @@ func (o *TelegramOutage) End() { return } - o.logger.Printf("%s after %s", o.back, durationText(time.Since(o.start))) + o.logger.Info(o.back, "after", time.Since(o.start)) + o.start = time.Time{} } @@ -557,6 +572,7 @@ func telegramLoadOffset(path string) (int64, error) { } var file TelegramOffsetFile + err = json.Unmarshal(data, &file) if err != nil { return 0, fmt.Errorf("%s: %w", path, err) @@ -588,6 +604,7 @@ func telegramSaveOffset(path string, offset int64) error { // never cuts a character. func telegramSplitMessage(text string, limit int) []string { var parts []string + for { end := telegramPrefixEnd(text, limit) if end == len(text) { @@ -637,6 +654,7 @@ func telegramText(event Event) string { // not cut an entity of telegramEscapeTags. It never cuts a character. func telegramSplitRich(markdown string, limit int) []string { var parts []string + for len(markdown) > limit { cut, fence, opening := telegramRichCut(markdown, limit) if fence == "" { @@ -692,7 +710,9 @@ func telegramPrefixEnd(text string, limit int) int { // so each cut makes the rest shorter. func telegramRichCut(markdown string, limit int) (int, string, string) { blank, line, code := 0, 0, 0 + var fence, opening, inFence, inOpening string + for start := 0; start < limit; { end := strings.IndexByte(markdown[start:], '\n') if end < 0 || start+end+1 > limit { @@ -701,10 +721,10 @@ func telegramRichCut(markdown string, limit int) (int, string, string) { end += start + 1 text := strings.TrimRight(markdown[start:end], "\r\n") - _, opens := telegramCodeFence(text) + _, opens := markdownCodeFence(text) switch { case fence != "": - if telegramClosesFence(text, fence) { + if markdownClosesFence(text, fence) { fence = "" } else if end > len(opening)+1 && end+len(fence)+1 <= limit { code, inFence, inOpening = end, fence, opening @@ -751,10 +771,13 @@ func telegramRichCut(markdown string, limit int) (int, string, string) { // An entity that crosses a cut becomes one entity in each part. func telegramSplitEntities(text string, entities []TelegramEntity, limit int) []TelegramPart { var parts []TelegramPart + start := 0 for _, part := range telegramSplitMessage(text, limit) { end := start + telegramUTF16Units(part) + var kept []TelegramEntity + for _, entity := range entities { from := max(entity.Offset, start) to := min(entity.Offset+entity.Length, end)