chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
746
internal/rpc/messages_webview.go
Normal file
746
internal/rpc/messages_webview.go
Normal file
|
|
@ -0,0 +1,746 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
func (r *Router) onMessagesRequestWebView(ctx context.Context, req *tg.MessagesRequestWebViewRequest) (*tg.WebViewResultURL, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if userID == 0 || r.deps.Users == nil || r.deps.Bots == nil {
|
||||
return nil, botInvalidErr()
|
||||
}
|
||||
user, err := r.deps.Users.Self(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
bot, profile, err := r.webViewBotFromInput(ctx, userID, req.Bot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawURL, err := webViewRequestURL(req.GetURL, req.FromBotMenu, profile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
startParam, err := webViewStartParam(req.GetStartParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateWebViewPlatform(req.Platform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
replyTo, err := r.messageReplyFromInput(ctx, userID, peer, req.ReplyTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sendAs, err := r.resolveSendAsPeer(ctx, userID, peer, req.SendAs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := r.clock.Now()
|
||||
session := r.webviews.registerContext(ctx, now, store.WebViewSession{
|
||||
BotUserID: bot.ID,
|
||||
UserID: userID,
|
||||
Peer: peer,
|
||||
Source: "webview",
|
||||
StartParam: startParam,
|
||||
Silent: req.Silent,
|
||||
ReplyTo: replyTo,
|
||||
SendAs: sendAs,
|
||||
})
|
||||
signedURL, err := webViewURLWithInitData(rawURL, session.BotQueryID, profile, user, startParam, req.Platform, now)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
out := &tg.WebViewResultURL{URL: signedURL}
|
||||
out.SetQueryID(session.QueryID)
|
||||
if req.Fullscreen {
|
||||
out.SetFullscreen(true)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesRequestSimpleWebView(ctx context.Context, req *tg.MessagesRequestSimpleWebViewRequest) (*tg.WebViewResultURL, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if userID == 0 || r.deps.Users == nil {
|
||||
return nil, botInvalidErr()
|
||||
}
|
||||
bot, found, err := r.userFromInput(ctx, userID, req.Bot)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found || !bot.Bot {
|
||||
return nil, botInvalidErr()
|
||||
}
|
||||
urlValue, ok := req.GetURL()
|
||||
if !ok || validateInlineWebURL(urlValue, true) != nil {
|
||||
return nil, urlInvalidErr()
|
||||
}
|
||||
if startParam, ok := req.GetStartParam(); ok && !validInlineStartParam(startParam) {
|
||||
return nil, startParamInvalidErr()
|
||||
}
|
||||
if req.Platform == "" || utf8.RuneCountInString(req.Platform) > domain.MaxBotInlineSwitchTextLen || strings.TrimSpace(req.Platform) != req.Platform {
|
||||
return nil, platformInvalidErr()
|
||||
}
|
||||
out := &tg.WebViewResultURL{URL: urlValue}
|
||||
if req.Fullscreen {
|
||||
out.SetFullscreen(true)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesGetBotApp(ctx context.Context, req *tg.MessagesGetBotAppRequest) (*tg.MessagesBotApp, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if userID == 0 || r.deps.Users == nil || r.deps.Bots == nil {
|
||||
return nil, botAppInvalidErr()
|
||||
}
|
||||
_, _, appDomain, app, _, err := r.webViewBotAppFromInput(ctx, userID, req.App)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &tg.MessagesBotApp{}
|
||||
if req.Hash != 0 && req.Hash == appDomain.Hash {
|
||||
out.App = &tg.BotAppNotModified{}
|
||||
return out, nil
|
||||
}
|
||||
out.App = app
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesRequestAppWebView(ctx context.Context, req *tg.MessagesRequestAppWebViewRequest) (*tg.WebViewResultURL, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if userID == 0 || r.deps.Users == nil || r.deps.Bots == nil {
|
||||
return nil, botAppInvalidErr()
|
||||
}
|
||||
user, err := r.deps.Users.Self(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
bot, profile, app, _, rawURL, err := r.webViewBotAppFromInput(ctx, userID, req.App)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
startParam, err := webViewStartParam(req.GetStartParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateWebViewPlatform(req.Platform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer, err := r.webViewPeerFromInput(ctx, userID, req.Peer, bot.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.WriteAllowed {
|
||||
created, err := r.deps.Bots.AllowSendMessage(ctx, userID, bot.ID, true)
|
||||
if err != nil {
|
||||
return nil, botInvalidErr()
|
||||
}
|
||||
if created {
|
||||
res, err := r.sendBotAllowedServiceMessageWith(ctx, userID, bot.ID, domain.MessageBotAllowedAction{FromRequest: true})
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !res.Duplicate {
|
||||
senderUsers := r.usersForMessageUpdate(ctx, userID, res.SenderMessage)
|
||||
senderChats := r.chatsForMessageUpdate(ctx, userID, res.SenderMessage)
|
||||
r.pushUserMessage(ctx, userID, "push webview write allowed", tgPrivateMessageUpdates(res.SenderEvent, res.SenderMessage, 0, false, senderUsers, senderChats))
|
||||
}
|
||||
}
|
||||
}
|
||||
now := r.clock.Now()
|
||||
session := r.webviews.registerContext(ctx, now, store.WebViewSession{
|
||||
BotUserID: bot.ID,
|
||||
UserID: userID,
|
||||
Peer: peer,
|
||||
AppID: app.ID,
|
||||
Source: "app",
|
||||
StartParam: startParam,
|
||||
WriteAllowed: req.WriteAllowed,
|
||||
})
|
||||
signedURL, err := webViewURLWithInitData(rawURL, session.BotQueryID, profile, user, startParam, req.Platform, now)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
out := &tg.WebViewResultURL{URL: signedURL}
|
||||
out.SetQueryID(session.QueryID)
|
||||
if req.Fullscreen {
|
||||
out.SetFullscreen(true)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesRequestMainWebView(ctx context.Context, req *tg.MessagesRequestMainWebViewRequest) (*tg.WebViewResultURL, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if userID == 0 || r.deps.Users == nil || r.deps.Bots == nil {
|
||||
return nil, botInvalidErr()
|
||||
}
|
||||
user, err := r.deps.Users.Self(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
bot, profile, err := r.webViewBotFromInput(ctx, userID, req.Bot)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var app domain.BotApp
|
||||
if mainApp, found, err := r.deps.Bots.GetMainBotApp(ctx, bot.ID); err != nil {
|
||||
return nil, internalErr()
|
||||
} else if found {
|
||||
app = mainApp
|
||||
} else {
|
||||
rawURL, err := webViewMenuAppURL(profile, botWebviewDisabledErr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
app = domain.BotApp{
|
||||
ID: bot.ID,
|
||||
BotUserID: bot.ID,
|
||||
ShortName: "main",
|
||||
Title: profile.MenuButton.Text,
|
||||
Description: profile.Description,
|
||||
URL: rawURL,
|
||||
AccessHash: menuBackedBotAppAccessHash(profile),
|
||||
Hash: menuBackedBotAppHash(bot, profile, "main"),
|
||||
Main: true,
|
||||
}
|
||||
}
|
||||
rawURL := app.URL
|
||||
startParam, err := webViewStartParam(req.GetStartParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateWebViewPlatform(req.Platform); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
peer, err := r.webViewPeerFromInput(ctx, userID, req.Peer, bot.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := r.clock.Now()
|
||||
session := r.webviews.registerContext(ctx, now, store.WebViewSession{
|
||||
BotUserID: bot.ID,
|
||||
UserID: userID,
|
||||
Peer: peer,
|
||||
AppID: app.ID,
|
||||
Source: "main",
|
||||
StartParam: startParam,
|
||||
})
|
||||
signedURL, err := webViewURLWithInitData(rawURL, session.BotQueryID, profile, user, startParam, req.Platform, now)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
out := &tg.WebViewResultURL{URL: signedURL}
|
||||
out.SetQueryID(session.QueryID)
|
||||
if req.Fullscreen {
|
||||
out.SetFullscreen(true)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesProlongWebView(ctx context.Context, req *tg.MessagesProlongWebViewRequest) (bool, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
if userID == 0 || r.deps.Users == nil || r.deps.Bots == nil || req.QueryID == 0 {
|
||||
return false, queryIDInvalidErr()
|
||||
}
|
||||
bot, _, err := r.webViewBotFromInput(ctx, userID, req.Bot)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
replyTo, err := r.messageReplyFromInput(ctx, userID, peer, req.ReplyTo)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
sendAs, err := r.resolveSendAsPeer(ctx, userID, peer, req.SendAs)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !r.webviews.prolongContext(ctx, r.clock.Now(), req.QueryID, userID, bot.ID, peer, req.Silent, replyTo, sendAs) {
|
||||
return false, queryIDInvalidErr()
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesSendWebViewResultMessage(ctx context.Context, req *tg.MessagesSendWebViewResultMessageRequest) (*tg.WebViewMessageSent, error) {
|
||||
botID, err := r.callerBotID(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.BotQueryID == "" {
|
||||
return nil, queryIDInvalidErr()
|
||||
}
|
||||
result, err := r.domainInlineResultFromTG(ctx, botID, req.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.sendWebViewDomainResultMessage(ctx, botID, req.BotQueryID, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tg.WebViewMessageSent{}, nil
|
||||
}
|
||||
|
||||
// AnswerWebAppQueryFromBotAPI answers a Bot API answerWebAppQuery request using
|
||||
// the same webview session registry and message pipeline as MTProto
|
||||
// messages.sendWebViewResultMessage. It intentionally accepts only domain
|
||||
// values, keeping Bot API parsing out of the MTProto/tg boundary.
|
||||
func (r *Router) AnswerWebAppQueryFromBotAPI(ctx context.Context, botID int64, botQueryID string, result domain.BotInlineResult) (string, error) {
|
||||
if botID == 0 || strings.TrimSpace(botQueryID) == "" {
|
||||
return "", queryIDInvalidErr()
|
||||
}
|
||||
if r.deps.Bots == nil {
|
||||
return "", userBotRequiredErr()
|
||||
}
|
||||
if _, found, err := r.deps.Bots.BotInfo(ctx, botID); err != nil {
|
||||
return "", internalErr()
|
||||
} else if !found {
|
||||
return "", userBotRequiredErr()
|
||||
}
|
||||
if err := r.sendWebViewDomainResultMessage(ctx, botID, botQueryID, result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// SavePreparedInlineMessageFromBotAPI stores a Bot API prepared inline message
|
||||
// in the same short-lived local/shared inline registry used by
|
||||
// messages.savePreparedInlineMessage.
|
||||
func (r *Router) SavePreparedInlineMessageFromBotAPI(ctx context.Context, botID, userID int64, result domain.BotInlineResult, peerTypes []string) (string, int, error) {
|
||||
if botID == 0 {
|
||||
return "", 0, userBotRequiredErr()
|
||||
}
|
||||
if userID == 0 || r.deps.Users == nil || r.deps.Bots == nil {
|
||||
return "", 0, userIDInvalidErr()
|
||||
}
|
||||
if _, found, err := r.deps.Bots.BotInfo(ctx, botID); err != nil {
|
||||
return "", 0, internalErr()
|
||||
} else if !found {
|
||||
return "", 0, userBotRequiredErr()
|
||||
}
|
||||
if _, found, err := r.deps.Users.ByID(ctx, botID, userID); err != nil {
|
||||
return "", 0, internalErr()
|
||||
} else if !found {
|
||||
return "", 0, userIDInvalidErr()
|
||||
}
|
||||
id, expireDate := r.inlines.savePreparedInlineContext(ctx, r.clock.Now(), botID, userID, result, peerTypes)
|
||||
return id, expireDate, nil
|
||||
}
|
||||
|
||||
func (r *Router) sendWebViewDomainResultMessage(ctx context.Context, botID int64, botQueryID string, result domain.BotInlineResult) error {
|
||||
now := r.clock.Now()
|
||||
session, ok := r.webviews.sessionForBotQueryContext(ctx, now, botID, botQueryID)
|
||||
if !ok {
|
||||
return queryIDInvalidErr()
|
||||
}
|
||||
if err := r.checkSendRateLimit(ctx, session.UserID, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
media := cloneInlineMedia(result.Media)
|
||||
if result.MediaAuto {
|
||||
if result.Content != nil {
|
||||
r.inlines.registerWebDocumentContext(ctx, now, *result.Content, webViewSessionTTL)
|
||||
}
|
||||
var err error
|
||||
media, err = r.domainInlineExternalContentMedia(ctx, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.resolveInlineContactMediaForSender(ctx, session.UserID, media)
|
||||
_, _, err := r.sendOutgoing(ctx, session.UserID, session.Peer, outgoingSend{
|
||||
randomID: randomNonZeroInt64(),
|
||||
message: result.Message,
|
||||
entities: tgMessageEntities(result.Entities),
|
||||
media: media,
|
||||
silent: session.Silent,
|
||||
replyTo: session.ReplyTo,
|
||||
replyToReady: true,
|
||||
sendAs: session.SendAs,
|
||||
sendAsReady: true,
|
||||
replyMarkup: result.ReplyMarkup,
|
||||
viaBotID: botID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.webviews.consumeContext(ctx, session.QueryID, session.BotQueryID)
|
||||
r.pushUserMessage(ctx, session.UserID, "push webview result sent", &tg.Updates{
|
||||
Updates: []tg.UpdateClass{&tg.UpdateWebViewResultSent{QueryID: session.QueryID}},
|
||||
Date: int(now.Unix()),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Router) webViewBotFromInput(ctx context.Context, userID int64, input tg.InputUserClass) (domain.User, domain.BotProfile, error) {
|
||||
bot, found, err := r.userFromInput(ctx, userID, input)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, internalErr()
|
||||
}
|
||||
if !found || !bot.Bot {
|
||||
return domain.User{}, domain.BotProfile{}, botInvalidErr()
|
||||
}
|
||||
profile, found, err := r.deps.Bots.BotInfo(ctx, bot.ID)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, internalErr()
|
||||
}
|
||||
if !found {
|
||||
return domain.User{}, domain.BotProfile{}, botInvalidErr()
|
||||
}
|
||||
return bot, profile, nil
|
||||
}
|
||||
|
||||
func (r *Router) webViewBotAppFromInput(ctx context.Context, userID int64, input tg.InputBotAppClass) (domain.User, domain.BotProfile, domain.BotApp, *tg.BotApp, string, error) {
|
||||
switch app := input.(type) {
|
||||
case *tg.InputBotAppShortName:
|
||||
if !validBotAppShortName(app.ShortName) {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", botAppShortNameInvalidErr()
|
||||
}
|
||||
bot, profile, err := r.webViewBotAppOwnerFromInput(ctx, userID, app.BotID)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", err
|
||||
}
|
||||
domainApp, found, err := r.deps.Bots.GetBotAppByShortName(ctx, bot.ID, app.ShortName)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", internalErr()
|
||||
}
|
||||
if !found {
|
||||
domainApp, err = r.ensureMenuBackedBotApp(ctx, bot, profile, app.ShortName)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", err
|
||||
}
|
||||
}
|
||||
if domainApp.Inactive || validateInlineWebURL(domainApp.URL, true) != nil {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", botAppInvalidErr()
|
||||
}
|
||||
return bot, profile, domainApp, r.tgBotAppFromDomain(ctx, domainApp), domainApp.URL, nil
|
||||
case *tg.InputBotAppID:
|
||||
if app.ID <= 0 {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", botAppInvalidErr()
|
||||
}
|
||||
domainApp, found, err := r.deps.Bots.GetBotAppByID(ctx, app.ID, app.AccessHash)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", internalErr()
|
||||
}
|
||||
if !found || domainApp.Inactive || validateInlineWebURL(domainApp.URL, true) != nil {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", botAppInvalidErr()
|
||||
}
|
||||
bot, found, err := r.deps.Users.ByID(ctx, userID, domainApp.BotUserID)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", internalErr()
|
||||
}
|
||||
if !found || !bot.Bot {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", botAppInvalidErr()
|
||||
}
|
||||
profile, found, err := r.deps.Bots.BotInfo(ctx, bot.ID)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", internalErr()
|
||||
}
|
||||
if !found {
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", botAppInvalidErr()
|
||||
}
|
||||
return bot, profile, domainApp, r.tgBotAppFromDomain(ctx, domainApp), domainApp.URL, nil
|
||||
default:
|
||||
return domain.User{}, domain.BotProfile{}, domain.BotApp{}, nil, "", botAppInvalidErr()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) ensureMenuBackedBotApp(ctx context.Context, bot domain.User, profile domain.BotProfile, shortName string) (domain.BotApp, error) {
|
||||
rawURL, err := webViewMenuAppURL(profile, botAppInvalidErr())
|
||||
if err != nil {
|
||||
return domain.BotApp{}, err
|
||||
}
|
||||
title := profile.MenuButton.Text
|
||||
if title == "" {
|
||||
title = bot.FirstName
|
||||
}
|
||||
app := domain.BotApp{
|
||||
BotUserID: bot.ID,
|
||||
ShortName: strings.ToLower(shortName),
|
||||
Title: title,
|
||||
Description: profile.Description,
|
||||
URL: rawURL,
|
||||
Main: strings.EqualFold(shortName, "main"),
|
||||
RequestWriteAccess: true,
|
||||
}
|
||||
out, _, err := r.deps.Bots.UpsertBotApp(ctx, bot.ID, app)
|
||||
if err != nil {
|
||||
return domain.BotApp{}, botAppInvalidErr()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) webViewBotAppOwnerFromInput(ctx context.Context, userID int64, input tg.InputUserClass) (domain.User, domain.BotProfile, error) {
|
||||
bot, found, err := r.userFromInput(ctx, userID, input)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, internalErr()
|
||||
}
|
||||
if !found || !bot.Bot {
|
||||
return domain.User{}, domain.BotProfile{}, botAppBotInvalidErr()
|
||||
}
|
||||
profile, found, err := r.deps.Bots.BotInfo(ctx, bot.ID)
|
||||
if err != nil {
|
||||
return domain.User{}, domain.BotProfile{}, internalErr()
|
||||
}
|
||||
if !found {
|
||||
return domain.User{}, domain.BotProfile{}, botAppBotInvalidErr()
|
||||
}
|
||||
return bot, profile, nil
|
||||
}
|
||||
|
||||
func (r *Router) webViewPeerFromInput(ctx context.Context, userID int64, input tg.InputPeerClass, fallbackBotID int64) (domain.Peer, error) {
|
||||
switch input.(type) {
|
||||
case nil, *tg.InputPeerEmpty:
|
||||
if fallbackBotID <= 0 {
|
||||
return domain.Peer{}, peerIDInvalidErr()
|
||||
}
|
||||
return domain.Peer{Type: domain.PeerTypeUser, ID: fallbackBotID}, nil
|
||||
default:
|
||||
return r.checkedDomainPeerFromInputPeer(ctx, userID, input)
|
||||
}
|
||||
}
|
||||
|
||||
func webViewRequestURL(getURL func() (string, bool), fromBotMenu bool, profile domain.BotProfile) (string, error) {
|
||||
rawURL, ok := getURL()
|
||||
if !ok {
|
||||
if fromBotMenu && profile.MenuButton.Type == domain.BotMenuButtonWebView {
|
||||
rawURL = profile.MenuButton.URL
|
||||
} else if fromBotMenu {
|
||||
return "", botWebviewDisabledErr()
|
||||
} else {
|
||||
return "", urlInvalidErr()
|
||||
}
|
||||
}
|
||||
if validateInlineWebURL(rawURL, true) != nil {
|
||||
return "", urlInvalidErr()
|
||||
}
|
||||
return rawURL, nil
|
||||
}
|
||||
|
||||
func webViewMenuAppURL(profile domain.BotProfile, disabledErr error) (string, error) {
|
||||
if profile.MenuButton.Type != domain.BotMenuButtonWebView || profile.MenuButton.URL == "" {
|
||||
return "", disabledErr
|
||||
}
|
||||
if validateInlineWebURL(profile.MenuButton.URL, true) != nil {
|
||||
return "", disabledErr
|
||||
}
|
||||
return profile.MenuButton.URL, nil
|
||||
}
|
||||
|
||||
func validBotAppShortName(shortName string) bool {
|
||||
if shortName == "" || len(shortName) > 64 {
|
||||
return false
|
||||
}
|
||||
for _, r := range shortName {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Router) tgBotAppFromDomain(ctx context.Context, in domain.BotApp) *tg.BotApp {
|
||||
out := &tg.BotApp{
|
||||
ID: in.ID,
|
||||
AccessHash: in.AccessHash,
|
||||
ShortName: in.ShortName,
|
||||
Title: in.Title,
|
||||
Description: in.Description,
|
||||
Photo: &tg.PhotoEmpty{ID: in.PhotoID},
|
||||
Hash: in.Hash,
|
||||
}
|
||||
if in.PhotoID != 0 && r.deps.Files != nil {
|
||||
if photo, found, err := r.deps.Files.GetPhoto(ctx, in.PhotoID); err == nil && found {
|
||||
out.Photo = tgPhoto(photo)
|
||||
}
|
||||
}
|
||||
if out.Photo == nil {
|
||||
out.Photo = &tg.PhotoEmpty{ID: in.ID}
|
||||
}
|
||||
if in.DocumentID != 0 && r.deps.Files != nil {
|
||||
if doc, found, err := r.deps.Files.GetDocument(ctx, in.DocumentID); err == nil && found {
|
||||
out.SetDocument(tgDocument(doc))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgMenuBackedBotApp(bot domain.User, profile domain.BotProfile, shortName string) *tg.BotApp {
|
||||
title := profile.MenuButton.Text
|
||||
if title == "" {
|
||||
title = bot.FirstName
|
||||
}
|
||||
app := &tg.BotApp{
|
||||
ID: bot.ID,
|
||||
AccessHash: menuBackedBotAppAccessHash(profile),
|
||||
ShortName: shortName,
|
||||
Title: title,
|
||||
Description: profile.Description,
|
||||
Photo: &tg.PhotoEmpty{ID: bot.ID},
|
||||
Hash: menuBackedBotAppHash(bot, profile, shortName),
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
func menuBackedBotAppAccessHash(profile domain.BotProfile) int64 {
|
||||
return stableBotAppInt64("access", strconv.FormatInt(profile.BotUserID, 10), profile.TokenSecret, profile.MenuButton.URL)
|
||||
}
|
||||
|
||||
func menuBackedBotAppHash(bot domain.User, profile domain.BotProfile, shortName string) int64 {
|
||||
return stableBotAppInt64(
|
||||
"hash",
|
||||
strconv.FormatInt(profile.BotUserID, 10),
|
||||
shortName,
|
||||
profile.MenuButton.Text,
|
||||
profile.MenuButton.URL,
|
||||
profile.Description,
|
||||
bot.FirstName,
|
||||
bot.Username,
|
||||
)
|
||||
}
|
||||
|
||||
func stableBotAppInt64(parts ...string) int64 {
|
||||
hash := sha256.New()
|
||||
for _, part := range parts {
|
||||
_, _ = hash.Write([]byte(part))
|
||||
_, _ = hash.Write([]byte{0})
|
||||
}
|
||||
sum := hash.Sum(nil)
|
||||
value := int64(binary.BigEndian.Uint64(sum[:8]) & 0x7fffffffffffffff)
|
||||
if value == 0 {
|
||||
return 1
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func webViewStartParam(getStartParam func() (string, bool)) (string, error) {
|
||||
startParam, ok := getStartParam()
|
||||
if ok && !validInlineStartParam(startParam) {
|
||||
return "", startParamInvalidErr()
|
||||
}
|
||||
return startParam, nil
|
||||
}
|
||||
|
||||
func validateWebViewPlatform(platform string) error {
|
||||
if platform == "" || utf8.RuneCountInString(platform) > domain.MaxBotInlineSwitchTextLen || strings.TrimSpace(platform) != platform {
|
||||
return platformInvalidErr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webViewURLWithInitData(rawURL, botQueryID string, profile domain.BotProfile, user domain.User, startParam, platform string, now time.Time) (string, error) {
|
||||
parsed, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
initData, err := signedWebViewInitData(botQueryID, profile, user, startParam, now)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q := parsed.Query()
|
||||
q.Set("tgWebAppData", initData)
|
||||
q.Set("tgWebAppVersion", "6.0")
|
||||
q.Set("tgWebAppPlatform", platform)
|
||||
parsed.RawQuery = q.Encode()
|
||||
return parsed.String(), nil
|
||||
}
|
||||
|
||||
func signedWebViewInitData(botQueryID string, profile domain.BotProfile, user domain.User, startParam string, now time.Time) (string, error) {
|
||||
userJSON, err := json.Marshal(webViewInitUser{
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Username: user.Username,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("query_id", botQueryID)
|
||||
values.Set("user", string(userJSON))
|
||||
values.Set("auth_date", strconv.FormatInt(now.Unix(), 10))
|
||||
if startParam != "" {
|
||||
values.Set("start_param", startParam)
|
||||
}
|
||||
token := domain.FormatBotToken(profile.BotUserID, profile.TokenSecret)
|
||||
values.Set("hash", webViewInitDataHash(values, token))
|
||||
return values.Encode(), nil
|
||||
}
|
||||
|
||||
type webViewInitUser struct {
|
||||
ID int64 `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
}
|
||||
|
||||
func webViewInitDataHash(values url.Values, botToken string) string {
|
||||
check := webViewDataCheckString(values)
|
||||
secretMAC := hmac.New(sha256.New, []byte("WebAppData"))
|
||||
_, _ = secretMAC.Write([]byte(botToken))
|
||||
secret := secretMAC.Sum(nil)
|
||||
dataMAC := hmac.New(sha256.New, secret)
|
||||
_, _ = dataMAC.Write([]byte(check))
|
||||
return hex.EncodeToString(dataMAC.Sum(nil))
|
||||
}
|
||||
|
||||
func webViewDataCheckString(values url.Values) string {
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
if key != "hash" {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
for i, key := range keys {
|
||||
if i > 0 {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString(key)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(values.Get(key))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue