feat: sync bot keyboards and callbacks
Sync telesrv b96f2dd (feat(bot): complete keyboards callbacks and durable delivery). Skipped private docs and preserved public README files per sync rules; normalized the appearance seed log label for public naming.
This commit is contained in:
parent
0c99ae0a9d
commit
bf965f610c
80 changed files with 7212 additions and 349 deletions
|
|
@ -2,6 +2,7 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
|
@ -67,6 +68,7 @@ func (r *Router) onMessagesSendWebViewData(ctx context.Context, req *tg.Messages
|
|||
if err != nil {
|
||||
return nil, messageSendErr(err)
|
||||
}
|
||||
r.enqueueBotAPIPrivateMessageUpdateAsync(ctx, res)
|
||||
var users []tg.UserClass
|
||||
var chats []tg.ChatClass
|
||||
if !res.Duplicate {
|
||||
|
|
@ -101,16 +103,27 @@ func (r *Router) onMessagesSendBotRequestedPeer(ctx context.Context, req *tg.Mes
|
|||
if !found || !botUser.Bot {
|
||||
return nil, botInvalidErr()
|
||||
}
|
||||
webAppReqID, ok := req.GetWebappReqID()
|
||||
if !ok || webAppReqID == "" {
|
||||
return nil, buttonDataInvalidErr()
|
||||
}
|
||||
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()
|
||||
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()
|
||||
|
|
@ -121,11 +134,17 @@ func (r *Router) onMessagesSendBotRequestedPeer(ctx context.Context, req *tg.Mes
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !requestedPeerTypeMatches(button.PeerType, resolved) {
|
||||
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
|
||||
|
|
@ -134,14 +153,18 @@ func (r *Router) onMessagesSendBotRequestedPeer(ctx context.Context, req *tg.Mes
|
|||
res, err := r.deps.Messages.SendPrivateText(ctx, userID, domain.SendPrivateTextRequest{
|
||||
SenderUserID: userID,
|
||||
RecipientUserID: botUser.ID,
|
||||
RandomID: botRequestedPeerServiceMessageRandomID(userID, botUser.ID, webAppReqID, button.ButtonID, peers),
|
||||
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,
|
||||
ButtonID: button.ButtonID,
|
||||
Peers: peers,
|
||||
Details: details,
|
||||
NameRequested: button.NameRequested,
|
||||
UsernameRequested: button.UsernameRequested,
|
||||
PhotoRequested: button.PhotoRequested,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -153,7 +176,10 @@ func (r *Router) onMessagesSendBotRequestedPeer(ctx context.Context, req *tg.Mes
|
|||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
_ = r.deps.Bots.DeleteRequestedWebViewButton(ctx, botUser.ID, userID, webAppReqID)
|
||||
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 {
|
||||
|
|
@ -163,6 +189,128 @@ func (r *Router) onMessagesSendBotRequestedPeer(ctx context.Context, req *tg.Mes
|
|||
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", "":
|
||||
|
|
@ -174,6 +322,97 @@ func requestedPeerTypeMatches(kind string, peer domain.Peer) bool {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue