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.
372 lines
11 KiB
Go
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()
|
|
}
|