(new): expand runtime APIs
Golang lint / lint (push) Successful in 58s
Golang lint / lint (pull_request) Successful in 13m10s

(fix): harden concurrent lifecycle
(tests): add regression coverage
(doc): update v1.2 guidance
This commit is contained in:
2026-08-20 11:08:45 +03:00
parent 29b208eeec
commit 24040fe164
37 changed files with 1129 additions and 157 deletions
+141 -34
View File
@@ -84,45 +84,59 @@ type MessageContext struct {
sceneRuntime sceneRuntime
observer Observer
eventEmitter func(context.Context, Event)
asyncTask func(func())
botID int64
ctx context.Context
}
func (ctx *MessageContext) buildEditMessageTextParams(messageID int, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) (tgapi.EditMessageText, error) {
params := tgapi.EditMessageText{}
switch {
case messageID > 0 && ctx.Msg != nil && ctx.Msg.Chat != nil:
params.MessageID = messageID
params.ChatID = ctx.Msg.Chat.ID
params.BusinessConnectionID = ctx.Msg.BusinessConnectionID
case ctx.InlineMsgID != "":
params.InlineMessageID = ctx.InlineMsgID
default:
return params, ErrEditTargetMissing
}
if parseMode != "" {
params.ParseMode = parseMode
}
if keyboard != nil {
params.ReplyMarkup = keyboard.Get()
}
return params, nil
}
// AnswerMessage represents a message sent or edited via MessageContext.
// It holds metadata to allow further editing or deletion.
type AnswerMessage struct {
// MessageID identifies the sent Telegram message.
MessageID int
// Text contains the text or caption sent with the message.
// For rich messages, it contains rendered HTML for v1 compatibility.
Text string
// RichHTML contains the rendered HTML of a rich message.
RichHTML string // Since: Bot API 10.2
// IsMedia reports whether the answer contains media.
IsMedia bool
ctx *MessageContext // internal back-reference
}
func (ctx *MessageContext) edit(messageID int, text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
params, err := ctx.buildEditMessageTextParams(messageID, keyboard, parseMode)
if err != nil {
ctx.Logger.Errorln(err)
return nil
}
if err := validateMessageText(text); err != nil {
ctx.Logger.Errorln(err)
return nil
}
params := tgapi.EditMessageText{
Text: text,
ParseMode: parseMode,
}
switch {
case messageID > 0 && ctx.Msg != nil:
params.MessageID = messageID
params.ChatID = ctx.Msg.Chat.ID
case ctx.InlineMsgID != "":
params.InlineMessageID = ctx.InlineMsgID
default:
ctx.Logger.Errorln(ErrEditTargetMissing)
return nil
}
if keyboard != nil {
params.ReplyMarkup = keyboard.Get()
}
params.Text = text
msg, _, err := ctx.API.EditMessageTextWithContext(ctx.Context(), params)
if err != nil {
ctx.Logger.Errorln(err)
@@ -193,9 +207,10 @@ func (ctx *MessageContext) editPhotoText(messageID int, text string, kb *InlineK
ParseMode: parseMode,
}
switch {
case messageID > 0 && ctx.Msg != nil:
case messageID > 0 && ctx.Msg != nil && ctx.Msg.Chat != nil:
params.ChatID = ctx.Msg.Chat.ID
params.MessageID = messageID
params.BusinessConnectionID = ctx.Msg.BusinessConnectionID
case ctx.InlineMsgID != "":
params.InlineMessageID = ctx.InlineMsgID
default:
@@ -245,7 +260,7 @@ func (m *AnswerMessage) EditCaptionKeyboardMarkdown(text string, kb *InlineKeybo
}
func (ctx *MessageContext) answer(text string, keyboard *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.Msg == nil {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
}
@@ -254,9 +269,10 @@ func (ctx *MessageContext) answer(text string, keyboard *InlineKeyboard, parseMo
return nil
}
params := tgapi.SendMessage{
ChatID: ctx.Msg.Chat.ID,
Text: text,
ParseMode: parseMode,
BusinessConnectionID: ctx.Msg.BusinessConnectionID,
ChatID: ctx.Msg.Chat.ID,
Text: text,
ParseMode: parseMode,
}
if keyboard != nil {
params.ReplyMarkup = keyboard.Get()
@@ -339,7 +355,7 @@ func (ctx *MessageContext) answerLong(text string, keyboard *InlineKeyboard, par
ctx.Logger.Errorln(ErrMessageSplitImpossible)
return nil
}
if ctx.Msg == nil {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
}
@@ -374,7 +390,7 @@ func (ctx *MessageContext) answerLong(text string, keyboard *InlineKeyboard, par
}
func (ctx *MessageContext) answerPhoto(photoID, text string, kb *InlineKeyboard, parseMode tgapi.ParseMode) *AnswerMessage {
if ctx.Msg == nil {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
}
@@ -383,10 +399,11 @@ func (ctx *MessageContext) answerPhoto(photoID, text string, kb *InlineKeyboard,
return nil
}
params := tgapi.SendPhoto{
ChatID: ctx.Msg.Chat.ID,
Caption: text,
ParseMode: parseMode,
Photo: photoID,
BusinessConnectionID: ctx.Msg.BusinessConnectionID,
ChatID: ctx.Msg.Chat.ID,
Caption: text,
ParseMode: parseMode,
Photo: photoID,
}
if kb != nil {
params.ReplyMarkup = kb.Get()
@@ -449,7 +466,7 @@ func (ctx *MessageContext) delete(messageID int) {
ctx.Logger.Errorln(ErrMessageIDZero)
return
}
if ctx.Msg == nil {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return
}
@@ -501,12 +518,14 @@ func (ctx *MessageContext) AnswerCallbackURL(u string) { ctx.answerCallbackQuery
// SendAction sends a chat action (typing, uploading_photo, etc.) to indicate bot activity.
func (ctx *MessageContext) SendAction(action tgapi.ChatActionType) {
if ctx.Msg == nil {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return
}
params := tgapi.SendChatAction{
ChatID: ctx.Msg.Chat.ID, Action: action,
BusinessConnectionID: ctx.Msg.BusinessConnectionID,
ChatID: ctx.Msg.Chat.ID,
Action: action,
}
if ctx.Msg.MessageThreadID > 0 {
params.MessageThreadID = ctx.Msg.MessageThreadID
@@ -543,7 +562,7 @@ func (ctx *MessageContext) error(err error) {
func (ctx *MessageContext) Error(err error) { ctx.error(err) }
func (ctx *MessageContext) newDraft(parseMode tgapi.ParseMode) *Draft {
if ctx.Msg == nil {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
}
@@ -827,7 +846,7 @@ func (ctx *MessageContext) UpsertKeyboardMarkdown(text string, keyboard *InlineK
}
func (ctx *MessageContext) richAnswer(rich tgapi.InputRichMessage, keyboard *InlineKeyboard) *AnswerMessage {
if ctx.Msg == nil {
if ctx.Msg == nil || ctx.Msg.Chat == nil {
ctx.Logger.Errorln(ErrMessageContextNil)
return nil
}
@@ -844,6 +863,9 @@ func (ctx *MessageContext) richAnswer(rich tgapi.InputRichMessage, keyboard *Inl
if ctx.Msg.DirectMessageTopic != nil {
params.DirectMessagesTopicID = ctx.Msg.DirectMessageTopic.TopicID
}
if ctx.Msg.BusinessConnectionID != "" {
params.BusinessConnectionID = ctx.Msg.BusinessConnectionID
}
msg, err := ctx.API.SendRichMessageWithContext(ctx.Context(), params)
if err != nil {
@@ -851,7 +873,11 @@ func (ctx *MessageContext) richAnswer(rich tgapi.InputRichMessage, keyboard *Inl
return nil
}
return &AnswerMessage{
MessageID: msg.MessageID, ctx: ctx, Text: rich.HTML, IsMedia: false,
MessageID: msg.MessageID,
Text: rich.HTML,
RichHTML: rich.HTML,
IsMedia: false,
ctx: ctx,
}
}
@@ -877,3 +903,84 @@ func (ctx *MessageContext) RichAnswer(blocks ...tgapi.InputRichBlock) *AnswerMes
func (ctx *MessageContext) RichAnswerKeyboard(keyboard *InlineKeyboard, blocks ...tgapi.InputRichBlock) *AnswerMessage {
return ctx.richBlocksAnswer(keyboard, blocks...)
}
func (ctx *MessageContext) editRich(messageID int, keyboard *InlineKeyboard, blocks ...tgapi.InputRichBlock) *AnswerMessage {
params, err := ctx.buildEditMessageTextParams(messageID, keyboard, "")
if err != nil {
ctx.Logger.Errorln(err)
return nil
}
rich, err := tgrich.BuildHTML(blocks...)
if err != nil {
ctx.Logger.Errorln(err)
return nil
}
params.RichMessage = &rich
msg, _, err := ctx.API.EditMessageTextWithContext(ctx.Context(), params)
if err != nil {
ctx.Logger.Errorln(err)
return nil
}
resultMessageID := messageID
if msg.MessageID > 0 {
resultMessageID = msg.MessageID
}
return &AnswerMessage{
MessageID: resultMessageID, Text: rich.HTML, RichHTML: rich.HTML, IsMedia: false, ctx: ctx,
}
}
func (ctx *MessageContext) editRichCallback(keyboard *InlineKeyboard, blocks ...tgapi.InputRichBlock) *AnswerMessage {
if ctx.CallbackMsgID == 0 && ctx.InlineMsgID == "" {
ctx.Logger.Errorln(ErrCallbackMessageMissing)
return nil
}
return ctx.editRich(ctx.CallbackMsgID, keyboard, blocks...)
}
func (ctx *MessageContext) upsertRichKeyboard(keyboard *InlineKeyboard, blocks ...tgapi.InputRichBlock) *AnswerMessage {
if ctx.IsCallback() {
if ctx.HasPhoto() {
rich, err := tgrich.BuildHTML(blocks...)
if err != nil {
ctx.Logger.Errorln(err)
return nil
}
ctx.CallbackDelete()
return ctx.richAnswer(rich, keyboard)
}
return ctx.editRichCallback(keyboard, blocks...)
}
return ctx.richBlocksAnswer(keyboard, blocks...)
}
// EditCallbackRich builds rich blocks and replaces the callback message content and inline keyboard.
// It doesn't upload local files referenced with attach://.
//
// Since: Bot API 10.2
func (ctx *MessageContext) EditCallbackRich(keyboard *InlineKeyboard, blocks ...tgapi.InputRichBlock) *AnswerMessage {
return ctx.editRichCallback(keyboard, blocks...)
}
// UpsertKeyboardRich builds rich blocks and either edits the callback message or sends a new message.
// Photo callback messages are replaced because their text content can't be edited directly.
// It doesn't upload local files referenced with attach://.
//
// Since: Bot API 10.2
func (ctx *MessageContext) UpsertKeyboardRich(keyboard *InlineKeyboard, blocks ...tgapi.InputRichBlock) *AnswerMessage {
return ctx.upsertRichKeyboard(keyboard, blocks...)
}
// EditRich builds rich blocks and replaces the message content without changing its inline keyboard.
// It doesn't upload local files referenced with attach://.
//
// Since: Bot API 10.2
func (m *AnswerMessage) EditRich(blocks ...tgapi.InputRichBlock) *AnswerMessage {
return m.ctx.editRich(m.MessageID, nil, blocks...)
}
// EditRichKeyboard builds rich blocks and replaces the message content and inline keyboard.
// It doesn't upload local files referenced with attach://.
//
// Since: Bot API 10.2
func (m *AnswerMessage) EditRichKeyboard(keyboard *InlineKeyboard, blocks ...tgapi.InputRichBlock) *AnswerMessage {
return m.ctx.editRich(m.MessageID, keyboard, blocks...)
}