owpengram-server/internal/rpc/messages_bot_no_state.go
Astra 80e99e8781 Don't 500 on channel sends with an unresolvable @mention
A channel post containing an @token that is not a syntactically valid
username (too short, leading digit, etc.) made messages.sendMessage return
500 INTERNAL_SERVER_ERROR: mentionedUserIDsFromMessage turned every
ResolveUsername error into internalErr().

Skip tokens that fail with ErrUsernameInvalid / ErrUsernameNotOccupied
instead, matching real Telegram (the message sends, the client renders the
mention and only fails to open it on tap). Only unexpected storage errors
still abort. Same fix applied to the bot send path.
2026-09-08 11:42:03 +01:00

372 lines
11 KiB
Go

package rpc
import (
"context"
"unicode/utf8"
"github.com/iamxvbaba/td/tg"
"telesrv/internal/domain"
)
func (r *Router) onMessagesSavePreparedInlineMessage(ctx context.Context, req *tg.MessagesSavePreparedInlineMessageRequest) (*tg.MessagesBotPreparedInlineMessage, error) {
botID, err := r.callerBotID(ctx)
if err != nil {
return nil, err
}
if req == nil || req.Result == nil {
return nil, resultIDInvalidErr()
}
if r.deps.Users == nil {
return nil, internalErr()
}
currentUserID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
target, found, err := r.userFromInput(ctx, currentUserID, req.UserID)
if err != nil {
return nil, internalErr()
}
if !found {
return nil, userIDInvalidErr()
}
result, err := r.domainInlineResultFromTG(ctx, botID, req.Result)
if err != nil {
return nil, err
}
if err := r.prepareTelegramLoginMarkup(ctx, botID, result.ReplyMarkup); err != nil {
return nil, replyMarkupErr(err)
}
peerTypes, err := preparedInlinePeerTypesFromTG(req.PeerTypes)
if err != nil {
return nil, err
}
id, expireDate := r.inlines.savePreparedInlineContext(ctx, r.clock.Now(), botID, target.ID, result, peerTypes)
return &tg.MessagesBotPreparedInlineMessage{
ID: id,
ExpireDate: expireDate,
}, nil
}
func preparedInlinePeerTypesFromTG(in []tg.InlineQueryPeerTypeClass) ([]string, error) {
if len(in) == 0 {
return nil, nil
}
out := make([]string, 0, len(in))
seen := make(map[string]struct{}, len(in))
for _, peerType := range in {
value := storeInlineQueryPeerType(peerType)
if value == "" {
return nil, peerIDInvalidErr()
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out, nil
}
func tgPreparedInlinePeerTypes(in []string) []tg.InlineQueryPeerTypeClass {
if len(in) == 0 {
return []tg.InlineQueryPeerTypeClass{}
}
out := make([]tg.InlineQueryPeerTypeClass, 0, len(in))
for _, peerType := range in {
if value, ok := tgInlineQueryPeerTypeFromStore(peerType); ok {
out = append(out, value)
}
}
return out
}
func (r *Router) onMessagesEditInlineBotMessage(ctx context.Context, req *tg.MessagesEditInlineBotMessageRequest) (bool, error) {
botID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if req == nil {
return false, messageIDInvalidErr()
}
if botID == 0 || !r.userIsBot(ctx, botID) || r.deps.Messages == nil {
return false, messageIDInvalidErr()
}
target, found, err := r.privateMessageFromInlineID(ctx, botID, req.ID)
if err != nil {
return false, err
}
if found {
return r.editPrivateInlineBotMessage(ctx, botID, target, req)
}
_, channelTarget, found, err := r.channelMessageFromInlineID(ctx, botID, req.ID)
if err != nil {
return false, err
}
if found {
return r.editChannelInlineBotMessage(ctx, botID, channelTarget, req)
}
return false, messageIDInvalidErr()
}
func (r *Router) editPrivateInlineBotMessage(ctx context.Context, botID int64, target domain.Message, req *tg.MessagesEditInlineBotMessageRequest) (bool, error) {
newMedia, err := r.inlineEditMedia(ctx, target.OwnerUserID, req)
if err != nil {
return false, err
}
message := target.Body
entities := append([]domain.MessageEntity(nil), target.Entities...)
richMessage := target.RichMessage
setRichMessage := false
rawRichMessage, hasRichMessage := req.GetRichMessage()
rawMessage, hasMessage := req.GetMessage()
if hasMessage && hasRichMessage {
return false, mediaInvalidErr()
}
if hasRichMessage {
richMessage, err = r.domainRichMessageFromInput(ctx, rawRichMessage)
if err != nil {
return false, err
}
if richMessage.IsZero() {
return false, richMessageInvalidErr()
}
message, entities, setRichMessage = "", nil, true
} else if hasMessage {
if rawMessage == "" && newMedia == nil && target.Media.IsZero() {
return false, messageEmptyErr()
}
if utf8.RuneCountInString(rawMessage) > maxSendMessageTextLength {
return false, messageTooLongErr()
}
rawEntities, _ := req.GetEntities()
if len(rawEntities) > maxMessageEntityCount {
return false, entitiesTooLongErr()
}
message = rawMessage
entities = domainMessageEntitiesForViewer(botID, rawEntities)
richMessage, setRichMessage = nil, true
} else if req.ReplyMarkup == nil && newMedia == nil {
return false, messageNotModifiedErr()
}
var replyMarkup *domain.MessageReplyMarkup
setReplyMarkup := false
if req.ReplyMarkup != nil {
var err error
replyMarkup, err = domainReplyMarkupForSender(req.ReplyMarkup, true)
if err != nil {
return false, replyMarkupErr(err)
}
if _, ok := req.ReplyMarkup.(*tg.ReplyInlineMarkup); ok {
setReplyMarkup = true
}
}
if setReplyMarkup {
if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil {
return false, replyMarkupErr(err)
}
}
_, err = r.deps.Messages.EditMessage(ctx, target.OwnerUserID, domain.EditMessageRequest{
OwnerUserID: target.OwnerUserID,
Peer: target.Peer,
ID: target.ID,
Message: message,
Entities: entities,
Media: newMedia,
EditDate: int(r.clock.Now().Unix()),
SetReplyMarkup: setReplyMarkup,
ReplyMarkup: replyMarkup,
SetRichMessage: setRichMessage,
RichMessage: richMessage,
ViaBotEditBotID: botID,
})
if err != nil {
return false, messageEditErr(err)
}
return true, nil
}
func (r *Router) editChannelInlineBotMessage(ctx context.Context, botID int64, target domain.ChannelMessage, req *tg.MessagesEditInlineBotMessageRequest) (bool, error) {
if r.deps.Channels == nil {
return false, messageIDInvalidErr()
}
newMedia, err := r.inlineEditMedia(ctx, target.SenderUserID, req)
if err != nil {
return false, err
}
message := target.Body
entities := append([]domain.MessageEntity(nil), target.Entities...)
var mentionUserIDs []int64
richMessage := target.RichMessage
setRichMessage := false
rawRichMessage, hasRichMessage := req.GetRichMessage()
rawMessage, hasMessage := req.GetMessage()
if hasMessage && hasRichMessage {
return false, mediaInvalidErr()
}
if hasRichMessage {
richMessage, err = r.domainRichMessageFromInput(ctx, rawRichMessage)
if err != nil {
return false, err
}
if richMessage.IsZero() {
return false, richMessageInvalidErr()
}
message, entities, setRichMessage = "", nil, true
} else if hasMessage {
if rawMessage == "" && newMedia == nil && target.Media.IsZero() {
return false, messageEmptyErr()
}
if utf8.RuneCountInString(rawMessage) > maxSendMessageTextLength {
return false, messageTooLongErr()
}
rawEntities, _ := req.GetEntities()
if len(rawEntities) > maxMessageEntityCount {
return false, entitiesTooLongErr()
}
message = rawMessage
entities = domainMessageEntitiesForViewer(botID, rawEntities)
richMessage, setRichMessage = nil, true
var err error
mentionUserIDs, err = r.mentionedUserIDsFromMessage(ctx, botID, message, rawEntities)
if err != nil {
return false, err
}
} else {
if req.ReplyMarkup == nil && newMedia == nil {
return false, messageNotModifiedErr()
}
mentionUserIDs, err = r.mentionedUserIDsFromDomainMessage(ctx, botID, message, entities)
if err != nil {
return false, err
}
}
var replyMarkup *domain.MessageReplyMarkup
setReplyMarkup := false
if req.ReplyMarkup != nil {
var err error
replyMarkup, err = domainReplyMarkupForSender(req.ReplyMarkup, true)
if err != nil {
return false, replyMarkupErr(err)
}
if _, ok := req.ReplyMarkup.(*tg.ReplyInlineMarkup); ok {
setReplyMarkup = true
}
}
if setReplyMarkup {
if err := r.prepareTelegramLoginMarkup(ctx, botID, replyMarkup); err != nil {
return false, replyMarkupErr(err)
}
}
res, err := r.deps.Channels.EditInlineBotMessage(ctx, botID, domain.EditChannelMessageRequest{
UserID: target.SenderUserID,
ChannelID: target.ChannelID,
ID: target.ID,
Message: message,
Entities: entities,
Media: newMedia,
MentionUserIDs: mentionUserIDs,
EditDate: int(r.clock.Now().Unix()),
SetReplyMarkup: setReplyMarkup,
ReplyMarkup: replyMarkup,
SetRichMessage: setRichMessage,
RichMessage: richMessage,
ViaBotEditBotID: botID,
})
if err != nil {
return false, channelEditErr(err)
}
r.enqueueChannelEditMessageFanout(ctx, target.SenderUserID, res)
return true, nil
}
func (r *Router) inlineEditMedia(ctx context.Context, userID int64, req *tg.MessagesEditInlineBotMessageRequest) (*domain.MessageMedia, error) {
input, ok := req.GetMedia()
if !ok || editMessageMediaCanDegradeToText(input) {
return nil, nil
}
media, err := r.resolveInputMedia(ctx, userID, input)
if err != nil {
return nil, err
}
if !inlineEditMediaAllowed(media) {
return nil, mediaInvalidErr()
}
return media, nil
}
func inlineEditMediaAllowed(media *domain.MessageMedia) bool {
if media == nil || media.IsZero() {
return false
}
switch media.Kind {
case domain.MessageMediaKindPhoto,
domain.MessageMediaKindDocument,
domain.MessageMediaKindContact,
domain.MessageMediaKindGeo,
domain.MessageMediaKindVenue:
return true
default:
return false
}
}
func (r *Router) mentionedUserIDsFromDomainMessage(ctx context.Context, currentUserID int64, message string, entities []domain.MessageEntity) ([]int64, error) {
if r.deps.Users == nil {
return nil, nil
}
identity, _ := r.deps.Users.(UserIdentityService)
seen := make(map[int64]struct{})
out := make([]int64, 0)
add := func(id int64) {
if id == 0 {
return
}
if _, ok := seen[id]; ok {
return
}
seen[id] = struct{}{}
out = append(out, id)
}
for _, entity := range entities {
if entity.Type == domain.MessageEntityMentionName {
add(entity.UserID)
if len(out) >= domain.MaxChannelMentionRecipients {
return out, nil
}
}
}
if identity != nil {
for _, username := range extractMentionUsernames(message, domain.MaxChannelMentionRecipients-len(out)) {
user, found, err := identity.ResolveUsername(ctx, currentUserID, username)
if err != nil {
if mentionResolveFatal(err) {
return nil, internalErr()
}
continue
}
if found {
add(user.ID)
}
if len(out) >= domain.MaxChannelMentionRecipients {
return out, nil
}
}
}
return out, nil
}
func (r *Router) onMessagesSetBotShippingResults(ctx context.Context, req *tg.MessagesSetBotShippingResultsRequest) (bool, error) {
if _, err := r.callerBotID(ctx); err != nil {
return false, err
}
return false, queryIDInvalidErr()
}
func (r *Router) onMessagesSetBotPrecheckoutResults(ctx context.Context, req *tg.MessagesSetBotPrecheckoutResultsRequest) (bool, error) {
if _, err := r.callerBotID(ctx); err != nil {
return false, err
}
return false, queryIDInvalidErr()
}