owpengram-server/internal/rpc/messages_bot_longtail.go

471 lines
15 KiB
Go

package rpc
import (
"context"
"fmt"
"strconv"
"strings"
"unicode/utf8"
"github.com/iamxvbaba/td/tg"
"telesrv/internal/domain"
)
func (r *Router) onMessagesSendWebViewData(ctx context.Context, req *tg.MessagesSendWebViewDataRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, botInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if r.deps.Messages == nil {
return nil, internalErr()
}
if req.RandomID == 0 ||
strings.TrimSpace(req.ButtonText) == "" ||
utf8.RuneCountInString(req.ButtonText) > domain.MaxWebViewDataButtonTextLen ||
len(req.Data) > domain.MaxWebViewDataPayloadLen {
return nil, buttonDataInvalidErr()
}
bot, err := r.botUserFromInput(ctx, userID, req.Bot)
if err != nil {
return nil, err
}
if bot.ID == userID {
return nil, botInvalidErr()
}
recipientBlocked, err := r.peerBlocksUser(ctx, userID, bot.ID)
if err != nil {
return nil, err
}
idempotencyFingerprint, err := rpcRequestFingerprint(req)
if err != nil {
return nil, internalErr()
}
sessionID, _ := SessionIDFrom(ctx)
res, err := r.deps.Messages.SendPrivateText(ctx, userID, domain.SendPrivateTextRequest{
SenderUserID: userID,
RecipientUserID: bot.ID,
RandomID: req.RandomID,
IdempotencyFingerprint: idempotencyFingerprint,
Media: &domain.MessageMedia{
Kind: domain.MessageMediaKindService,
ServiceAction: &domain.MessageServiceAction{
Kind: domain.MessageServiceActionWebViewDataSent,
WebViewData: &domain.MessageWebViewDataAction{
ButtonText: req.ButtonText,
Data: req.Data,
},
},
},
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, messageSendErr(err)
}
r.enqueueBotAPIPrivateMessageUpdateAsync(ctx, res)
var users []tg.UserClass
var chats []tg.ChatClass
if !res.Duplicate {
users = r.usersForMessageUpdate(ctx, userID, res.SenderMessage)
chats = r.chatsForMessageUpdate(ctx, userID, res.SenderMessage)
}
return tgPrivateSendResultUpdates(res, req.RandomID, false, users, chats), nil
}
func (r *Router) onMessagesSendBotRequestedPeer(ctx context.Context, req *tg.MessagesSendBotRequestedPeerRequest) (tg.UpdatesClass, error) {
if req == nil {
return nil, buttonDataInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if r.deps.Bots == nil || r.deps.Messages == nil || r.deps.Users == nil {
return nil, internalErr()
}
botPeer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
if botPeer.Type != domain.PeerTypeUser {
return nil, botInvalidErr()
}
botUser, found, err := r.deps.Users.ByID(ctx, userID, botPeer.ID)
if err != nil {
return nil, internalErr()
}
if !found || !botUser.Bot {
return nil, botInvalidErr()
}
webAppReqID, fromWebApp := req.GetWebappReqID()
idempotencyKey := webAppReqID
var button domain.BotRequestedWebViewButton
if fromWebApp {
if webAppReqID == "" {
return nil, buttonDataInvalidErr()
}
var found bool
button, found, err = r.deps.Bots.GetRequestedWebViewButton(ctx, botUser.ID, userID, webAppReqID)
if err != nil {
return nil, internalErr()
}
if !found || button.ButtonID != req.ButtonID {
return nil, buttonDataInvalidErr()
}
} else {
idempotencyKey = "message:" + strconv.Itoa(req.MsgID)
button, err = r.requestPeerButtonFromMessage(ctx, userID, botUser.ID, req.MsgID, req.ButtonID)
if err != nil {
return nil, err
}
}
if len(req.RequestedPeers) == 0 || len(req.RequestedPeers) > button.MaxQuantity {
return nil, buttonDataInvalidErr()
}
peers := make([]domain.Peer, 0, len(req.RequestedPeers))
for _, peer := range req.RequestedPeers {
resolved, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
if err != nil {
return nil, err
}
if matches, err := r.requestedPeerMatches(ctx, userID, botUser.ID, button, resolved); err != nil {
return nil, internalErr()
} else if !matches {
return nil, buttonDataInvalidErr()
}
peers = append(peers, resolved)
}
details, err := r.requestedPeerDetails(ctx, userID, peers, button)
if err != nil {
return nil, internalErr()
}
recipientBlocked, err := r.peerBlocksUser(ctx, userID, botUser.ID)
if err != nil {
return nil, err
}
sessionID, _ := SessionIDFrom(ctx)
res, err := r.deps.Messages.SendPrivateText(ctx, userID, domain.SendPrivateTextRequest{
SenderUserID: userID,
RecipientUserID: botUser.ID,
RandomID: botRequestedPeerServiceMessageRandomID(userID, botUser.ID, idempotencyKey, button.ButtonID, peers),
Media: &domain.MessageMedia{
Kind: domain.MessageMediaKindService,
ServiceAction: &domain.MessageServiceAction{
Kind: domain.MessageServiceActionRequestedPeer,
RequestedPeer: &domain.MessageRequestedPeerAction{
ButtonID: button.ButtonID,
Peers: peers,
Details: details,
NameRequested: button.NameRequested,
UsernameRequested: button.UsernameRequested,
PhotoRequested: button.PhotoRequested,
},
},
},
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
if err != nil {
return nil, internalErr()
}
r.enqueueBotAPIPrivateMessageUpdateAsync(ctx, res)
if fromWebApp {
_ = r.deps.Bots.DeleteRequestedWebViewButton(ctx, botUser.ID, userID, webAppReqID)
}
var users []tg.UserClass
var chats []tg.ChatClass
if !res.Duplicate {
users = r.usersForMessageUpdate(ctx, userID, res.SenderMessage)
chats = r.chatsForMessageUpdate(ctx, userID, res.SenderMessage)
}
return tgPrivateSendResultUpdates(res, res.SenderMessage.RandomID, false, users, chats), nil
}
type requestedPeerPhotoProvider interface {
GetPhotos(ctx context.Context, ids []int64) ([]domain.Photo, error)
}
func (r *Router) requestedPeerDetails(ctx context.Context, viewerUserID int64, peers []domain.Peer, button domain.BotRequestedWebViewButton) ([]domain.MessageRequestedPeerDetails, error) {
details := make([]domain.MessageRequestedPeerDetails, len(peers))
for i, peer := range peers {
details[i].Peer = peer
}
if !button.NameRequested && !button.UsernameRequested && !button.PhotoRequested {
return details, nil
}
userIDs := make(map[int64]struct{})
channelIDs := make(map[int64]struct{})
for _, peer := range peers {
addDomainPeerRef(peer, 0, userIDs, channelIDs)
}
cache := newViewerPeerCache(r)
users := cache.usersForIDs(ctx, viewerUserID, mapKeys(userIDs))
channels := cache.channelsForIDs(ctx, viewerUserID, mapKeys(channelIDs))
userByID := make(map[int64]domain.User, len(users))
channelByID := make(map[int64]domain.Channel, len(channels))
photoIDs := make([]int64, 0, len(peers))
for _, user := range users {
userByID[user.ID] = user
if button.PhotoRequested && user.PhotoID != 0 {
photoIDs = append(photoIDs, user.PhotoID)
}
}
for _, channel := range channels {
channelByID[channel.ID] = channel
if button.PhotoRequested && channel.PhotoID != 0 {
photoIDs = append(photoIDs, channel.PhotoID)
}
}
photoByID := make(map[int64]domain.Photo, len(photoIDs))
if len(photoIDs) > 0 {
provider, ok := r.deps.Files.(requestedPeerPhotoProvider)
if !ok {
return nil, fmt.Errorf("requested peer photo provider unavailable")
}
photos, err := provider.GetPhotos(ctx, photoIDs)
if err != nil {
return nil, err
}
for _, photo := range photos {
photoByID[photo.ID] = photo
}
}
for i, peer := range peers {
detail := &details[i]
switch peer.Type {
case domain.PeerTypeUser:
user, ok := userByID[peer.ID]
if !ok {
return nil, fmt.Errorf("requested user %d not hydrated", peer.ID)
}
if button.NameRequested {
detail.FirstName, detail.LastName = user.FirstName, user.LastName
}
if button.UsernameRequested {
detail.Username = user.Username
}
if button.PhotoRequested && user.PhotoID != 0 {
photo, ok := photoByID[user.PhotoID]
if !ok {
return nil, fmt.Errorf("requested user photo %d missing", user.PhotoID)
}
detail.Photo = &photo
}
case domain.PeerTypeChannel:
channel, ok := channelByID[peer.ID]
if !ok {
return nil, fmt.Errorf("requested channel %d not hydrated", peer.ID)
}
if button.NameRequested {
detail.Title = channel.Title
}
if button.UsernameRequested {
detail.Username = channel.Username
}
if button.PhotoRequested && channel.PhotoID != 0 {
photo, ok := photoByID[channel.PhotoID]
if !ok {
return nil, fmt.Errorf("requested channel photo %d missing", channel.PhotoID)
}
detail.Photo = &photo
}
}
}
return details, nil
}
func (r *Router) requestPeerButtonFromMessage(ctx context.Context, userID, botUserID int64, messageID, buttonID int) (domain.BotRequestedWebViewButton, error) {
if messageID <= 0 || messageID > domain.MaxMessageBoxID || buttonID == 0 {
return domain.BotRequestedWebViewButton{}, buttonDataInvalidErr()
}
message, found, err := r.lookupOwnerMessage(ctx, userID, messageID)
if err != nil {
return domain.BotRequestedWebViewButton{}, internalErr()
}
if !found || message.Peer != (domain.Peer{Type: domain.PeerTypeUser, ID: botUserID}) ||
message.From != (domain.Peer{Type: domain.PeerTypeUser, ID: botUserID}) || message.ReplyMarkup == nil ||
message.ReplyMarkup.Kind() != domain.MessageReplyMarkupKeyboard {
return domain.BotRequestedWebViewButton{}, buttonDataInvalidErr()
}
for _, row := range message.ReplyMarkup.Keyboard {
for _, item := range row {
if item.Type != domain.MarkupButtonRequestPeer || item.ButtonID != buttonID {
continue
}
return domain.BotRequestedWebViewButton{
BotUserID: botUserID, UserID: userID, ButtonID: item.ButtonID,
PeerType: item.RequestPeerType, MaxQuantity: item.MaxQuantity, PeerFilter: item.RequestPeerFilter,
NameRequested: item.NameRequested, UsernameRequested: item.UsernameRequested,
PhotoRequested: item.PhotoRequested,
}, nil
}
}
return domain.BotRequestedWebViewButton{}, buttonDataInvalidErr()
}
func requestedPeerTypeMatches(kind string, peer domain.Peer) bool {
switch kind {
case "user", "":
return peer.Type == domain.PeerTypeUser
case "chat", "broadcast":
return peer.Type == domain.PeerTypeChannel
default:
return false
}
}
func (r *Router) requestedPeerMatches(ctx context.Context, userID, botUserID int64, button domain.BotRequestedWebViewButton, peer domain.Peer) (bool, error) {
if !requestedPeerTypeMatches(button.PeerType, peer) {
return false, nil
}
filter := button.PeerFilter
if filter == nil {
return true, nil
}
if peer.Type == domain.PeerTypeUser {
if r.deps.Users == nil {
return false, nil
}
user, found, err := r.deps.Users.ByID(ctx, userID, peer.ID)
if err != nil || !found {
return false, err
}
if filter.UserIsBotSet && user.Bot != filter.UserIsBot {
return false, nil
}
if filter.UserIsPremiumSet && user.PremiumActiveAt(r.clock.Now().Unix()) != filter.UserIsPremium {
return false, nil
}
return true, nil
}
if r.deps.Channels == nil {
return false, nil
}
view, err := r.deps.Channels.ResolveChannel(ctx, userID, peer.ID)
if err != nil {
return false, err
}
channel := view.Channel
if button.PeerType == "chat" && (!channel.Megagroup || channel.Broadcast) {
return false, nil
}
if button.PeerType == "broadcast" && !channel.Broadcast {
return false, nil
}
if filter.ChatHasUsernameSet && (channel.Username != "") != filter.ChatHasUsername {
return false, nil
}
if filter.ChatIsForumSet && channel.Forum != filter.ChatIsForum {
return false, nil
}
if filter.ChatIsCreated && view.Self.Role != domain.ChannelRoleCreator {
return false, nil
}
if filter.UserAdminRights != nil && !channelMemberHasRequestRights(view.Self, *filter.UserAdminRights) {
return false, nil
}
if filter.BotIsMember || filter.BotAdminRights != nil {
botMember, err := r.deps.Channels.GetParticipant(ctx, userID, peer.ID, botUserID)
if err != nil {
return false, err
}
if botMember.Status != domain.ChannelMemberActive {
return false, nil
}
if filter.BotAdminRights != nil && !channelMemberHasRequestRights(botMember, *filter.BotAdminRights) {
return false, nil
}
}
return true, nil
}
func channelMemberHasRequestRights(member domain.ChannelMember, required domain.BotRequestAdminRights) bool {
if member.Role == domain.ChannelRoleCreator {
return true
}
if member.Role != domain.ChannelRoleAdmin {
return false
}
rights := member.AdminRights
return (!required.Anonymous || rights.Anonymous) &&
(!required.ManageChat || rights.ManageChat) &&
(!required.DeleteMessages || rights.DeleteMessages) &&
(!required.ManageVideoChats || rights.ManageCall) &&
(!required.RestrictMembers || rights.BanUsers) &&
(!required.PromoteMembers || rights.AddAdmins) &&
(!required.ChangeInfo || rights.ChangeInfo) &&
(!required.InviteUsers || rights.InviteUsers) &&
(!required.PostStories || rights.PostStories) &&
(!required.EditStories || rights.EditStories) &&
(!required.DeleteStories || rights.DeleteStories) &&
(!required.PostMessages || rights.PostMessages) &&
(!required.EditMessages || rights.EditMessages) &&
(!required.PinMessages || rights.PinMessages) &&
(!required.ManageTopics || rights.ManageTopics) &&
(!required.ManageDirectMessages || rights.ManageDirectMessages)
}
func botRequestedPeerServiceMessageRandomID(userID, botUserID int64, reqID string, buttonID int, peers []domain.Peer) int64 {
parts := []string{"bot-requested-peer", strconv.FormatInt(userID, 10), strconv.FormatInt(botUserID, 10), reqID, strconv.Itoa(buttonID)}
for _, peer := range peers {
parts = append(parts, string(peer.Type), strconv.FormatInt(peer.ID, 10))
}
return -stableBotAppInt64(parts...)
}
func (r *Router) onMessagesGetPreparedInlineMessage(ctx context.Context, req *tg.MessagesGetPreparedInlineMessageRequest) (*tg.MessagesPreparedInlineMessage, error) {
if req == nil {
return nil, botInvalidErr()
}
if req.ID == "" {
return nil, resultIDEmptyErr()
}
if len(req.ID) > domain.MaxBotPreparedInlineIDLen {
return nil, resultIDInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
bot, err := r.botUserFromInput(ctx, userID, req.Bot)
if err != nil {
return nil, err
}
results, ok := r.inlines.preparedInlineContext(ctx, r.clock.Now(), userID, bot.ID, req.ID)
if !ok || len(results.Results) != 1 {
return nil, resultIDInvalidErr()
}
out := &tg.MessagesPreparedInlineMessage{
QueryID: results.QueryID,
Result: tgBotInlineResult(results.Results[0]),
PeerTypes: tgPreparedInlinePeerTypes(results.PeerTypes),
CacheTime: results.CacheTime,
Users: []tg.UserClass{},
}
if r.deps.Users != nil {
if u, found, err := r.deps.Users.ByID(ctx, userID, bot.ID); err == nil && found {
out.Users = append(out.Users, r.tgUser(u))
}
}
r.applyPeerReadModels(ctx, userID, out.Users, nil)
return out, nil
}
func (r *Router) botUserFromInput(ctx context.Context, userID int64, bot tg.InputUserClass) (domain.User, error) {
u, found, err := r.userFromInput(ctx, userID, bot)
if err != nil {
return domain.User{}, internalErr()
}
if !found || !u.Bot {
return domain.User{}, botInvalidErr()
}
return u, nil
}