owpengram-server/internal/rpc/messages_webview.go

746 lines
23 KiB
Go

package rpc
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"net/url"
"sort"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/iamxvbaba/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()
}