+// deliver sends part to the chat. When Telegram refuses a part with entities
+// (HTTP 400), it sends the text of the part again once with no entities, so a
+// reply is not lost to a bad entity. It tries again after each failure that a
+// later try can fix, until ctx ends. It drops the part after any other failure.
+func (b *TelegramBot) deliver(ctx context.Context, part TelegramPart) {
+ outage := b.sendOutage()
+ wait := b.minWait
+ for {
+ err := b.api.SendMessage(ctx, b.owner, part.text, part.entities)
+ retry, asked := telegramRetryable(err)
+ if !retry {
+ outage.End()
+ }
+
+ if err == nil || ctx.Err() != nil {
+ return
+ }
+
+ 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)
+
+ part.entities = nil
+
+ continue
+ }
+
+ if !retry {
+ b.logger.Printf("reply lost: %v", err)
+
+ return
+ }
+
+ wait = b.pause(ctx, outage, err, wait, asked)
+ }
+}
+
+// deliverRich sends markdown, escaped with telegramEscapeTags, to the chat as a
+// rich message. When the Bot API refuses it with HTTP 400 or 404, the message
+// was not sent, so deliverRich sends the text of the part once with message
+// entities, and the reply is not lost and does not repeat. Message entities
+// read no HTML, so the fallback gets the Markdown of telegramUnescapeTags, as
+// the model wrote it. It tries again after each failure that a later try can
+// fix, until ctx ends. It drops the message after any other failure.
+func (b *TelegramBot) deliverRich(ctx context.Context, markdown string) {
+ outage := b.sendOutage()
+ wait := b.minWait
+ for {
+ err := b.api.SendRichMessage(ctx, b.owner, markdown)
+ retry, asked := telegramRetryable(err)
+ if !retry {
+ outage.End()
+ }
+
+ if err == nil || ctx.Err() != nil {
+ return
+ }
+
+ if telegramRejected(err) {
+ b.logger.Printf("reply sent with simple formatting: %v", err)
+
+ for _, part := range telegramReplyParts(telegramUnescapeTags(markdown)) {
+ b.deliver(ctx, part)
+ }
+
+ return
+ }
+
+ if !retry {
+ b.logger.Printf("reply lost: %v", err)
+
+ return
+ }
+
+ wait = b.pause(ctx, outage, err, wait, asked)
+ }
+}
+
+// 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",
+ }
+}
+