owpengram-server/internal/app/bots/miniapp.go

373 lines
12 KiB
Go

package bots
import (
"context"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"net/url"
"strings"
"time"
"unicode/utf8"
"telesrv/internal/domain"
)
const (
defaultMainAppShortName = "main"
requestedWebViewButtonTTL = 10 * time.Minute
webViewCustomMethodQueryTTL = 5 * time.Minute
)
func (s *Service) UpsertBotApp(ctx context.Context, botUserID int64, app domain.BotApp) (domain.BotApp, int, error) {
clean, err := s.normalizeBotApp(ctx, botUserID, app)
if err != nil {
return domain.BotApp{}, 0, err
}
out, version, err := s.bots.UpsertBotApp(ctx, clean)
if err != nil {
return domain.BotApp{}, 0, err
}
s.invalidateBotReadCaches(ctx, botUserID)
return out, version, nil
}
func (s *Service) EnsureMenuBotApp(ctx context.Context, botUserID int64, button domain.BotMenuButton) (domain.BotApp, int, error) {
if button.Type != domain.BotMenuButtonWebView {
return domain.BotApp{}, 0, nil
}
app, version, err := s.UpsertBotApp(ctx, botUserID, domain.BotApp{
BotUserID: botUserID,
ShortName: defaultMainAppShortName,
Title: button.Text,
URL: button.URL,
Main: true,
RequestWriteAccess: true,
})
if err != nil {
return domain.BotApp{}, 0, err
}
if _, err := s.UpsertAttachMenuBot(ctx, botUserID, domain.BotAttachMenuBot{
BotUserID: botUserID,
AppID: app.ID,
ShortName: app.ShortName,
RequestWriteAccess: app.RequestWriteAccess,
ShowInAttachMenu: true,
ShowInSideMenu: true,
}); err != nil {
return domain.BotApp{}, 0, err
}
return app, version, nil
}
func (s *Service) normalizeBotApp(ctx context.Context, botUserID int64, app domain.BotApp) (domain.BotApp, error) {
if s == nil || s.bots == nil || botUserID == 0 {
return domain.BotApp{}, domain.ErrBotAppInvalid
}
app.BotUserID = botUserID
app.ShortName = strings.ToLower(strings.TrimSpace(app.ShortName))
app.Title = strings.TrimSpace(app.Title)
app.Description = strings.TrimSpace(app.Description)
app.URL = strings.TrimSpace(app.URL)
if !validBotAppShortName(app.ShortName) {
return domain.BotApp{}, domain.ErrBotAppShortNameInvalid
}
if app.Title == "" || utf8.RuneCountInString(app.Title) > domain.MaxBotAppTitleLen ||
utf8.RuneCountInString(app.Description) > domain.MaxBotAppDescriptionLen ||
len(app.URL) > domain.MaxBotAppURLLen || !validHTTPSURL(app.URL) {
return domain.BotApp{}, domain.ErrBotAppInvalid
}
if app.ID == 0 {
app.ID = stableBotAppInt64("bot-app-id", fmt.Sprint(botUserID), app.ShortName)
}
if app.AccessHash == 0 {
if existing, found, err := s.bots.GetBotAppByShortName(ctx, botUserID, app.ShortName); err == nil && found {
app.AccessHash = existing.AccessHash
if app.ID == 0 {
app.ID = existing.ID
}
}
}
if app.AccessHash == 0 {
app.AccessHash = stableBotAppInt64("bot-app-access", fmt.Sprint(botUserID), app.ShortName, app.URL)
}
app.Hash = botAppHash(app)
return app, nil
}
func (s *Service) GetBotAppByID(ctx context.Context, appID, accessHash int64) (domain.BotApp, bool, error) {
if s == nil || s.bots == nil {
return domain.BotApp{}, false, nil
}
return s.bots.GetBotAppByID(ctx, appID, accessHash)
}
func (s *Service) GetBotAppByShortName(ctx context.Context, botUserID int64, shortName string) (domain.BotApp, bool, error) {
if s == nil || s.bots == nil {
return domain.BotApp{}, false, nil
}
return s.bots.GetBotAppByShortName(ctx, botUserID, strings.ToLower(strings.TrimSpace(shortName)))
}
func (s *Service) GetMainBotApp(ctx context.Context, botUserID int64) (domain.BotApp, bool, error) {
if s == nil || s.bots == nil {
return domain.BotApp{}, false, nil
}
return s.bots.GetMainBotApp(ctx, botUserID)
}
func (s *Service) ListBotApps(ctx context.Context, botUserID int64) ([]domain.BotApp, error) {
if s == nil || s.bots == nil {
return nil, nil
}
return s.bots.ListBotApps(ctx, botUserID)
}
func (s *Service) GetBotAppSettings(ctx context.Context, botUserID int64) (domain.BotAppSettings, bool, error) {
if s == nil || s.bots == nil {
return domain.BotAppSettings{}, false, nil
}
return s.bots.GetBotAppSettings(ctx, botUserID)
}
func (s *Service) UpsertBotAppSettings(ctx context.Context, botUserID int64, settings domain.BotAppSettings) (int, error) {
if s == nil || s.bots == nil || botUserID == 0 {
return 0, domain.ErrBotAppInvalid
}
version, err := s.bots.UpsertBotAppSettings(ctx, botUserID, settings)
if err != nil {
return 0, err
}
s.invalidateBotReadCaches(ctx, botUserID)
return version, nil
}
func (s *Service) ListBotAppPreviewMedia(ctx context.Context, botUserID, appID int64) ([]domain.BotAppPreviewMedia, error) {
if s == nil || s.bots == nil {
return nil, nil
}
return s.bots.ListBotAppPreviewMedia(ctx, botUserID, appID)
}
func (s *Service) UpsertBotAppPreviewMedia(ctx context.Context, media domain.BotAppPreviewMedia) (domain.BotAppPreviewMedia, int, error) {
if s == nil || s.bots == nil {
return domain.BotAppPreviewMedia{}, 0, domain.ErrBotAppInvalid
}
if media.ID == 0 {
items, err := s.bots.ListBotAppPreviewMedia(ctx, media.BotUserID, media.AppID)
if err != nil {
return domain.BotAppPreviewMedia{}, 0, err
}
if len(items) >= domain.MaxBotPreviewMedia {
return domain.BotAppPreviewMedia{}, 0, domain.ErrBotAppInvalid
}
}
out, version, err := s.bots.UpsertBotAppPreviewMedia(ctx, media)
if err != nil {
return domain.BotAppPreviewMedia{}, 0, err
}
s.invalidateBotReadCaches(ctx, media.BotUserID)
return out, version, nil
}
func (s *Service) DeleteBotAppPreviewMedia(ctx context.Context, botUserID, appID, mediaID int64) (int, error) {
version, err := s.bots.DeleteBotAppPreviewMedia(ctx, botUserID, appID, mediaID)
if err != nil {
return 0, err
}
s.invalidateBotReadCaches(ctx, botUserID)
return version, nil
}
func (s *Service) ReorderBotAppPreviewMedia(ctx context.Context, botUserID, appID int64, mediaIDs []int64) (int, error) {
version, err := s.bots.ReorderBotAppPreviewMedia(ctx, botUserID, appID, mediaIDs)
if err != nil {
return 0, err
}
s.invalidateBotReadCaches(ctx, botUserID)
return version, nil
}
func (s *Service) UpsertAttachMenuBot(ctx context.Context, botUserID int64, bot domain.BotAttachMenuBot) (int, error) {
if s == nil || s.bots == nil || botUserID == 0 {
return 0, domain.ErrBotAttachMenuInvalid
}
bot.BotUserID = botUserID
bot.ShortName = strings.ToLower(strings.TrimSpace(bot.ShortName))
if bot.ShortName == "" {
if app, found, err := s.GetMainBotApp(ctx, botUserID); err == nil && found {
bot.AppID = app.ID
bot.ShortName = app.ShortName
bot.HasSettings = app.HasSettings
bot.RequestWriteAccess = app.RequestWriteAccess
}
}
if !validBotAppShortName(bot.ShortName) {
return 0, domain.ErrBotAttachMenuInvalid
}
if len(bot.PeerTypes) == 0 {
bot.PeerTypes = []string{"pm", "chat", "megagroup", "broadcast"}
}
if len(bot.PeerTypes) > domain.MaxBotAttachMenuPeerTypes || len(bot.Icons) > domain.MaxBotAttachMenuIcons {
return 0, domain.ErrBotAttachMenuInvalid
}
if !bot.ShowInAttachMenu && !bot.ShowInSideMenu {
bot.ShowInAttachMenu = true
bot.ShowInSideMenu = true
}
version, err := s.bots.UpsertAttachMenuBot(ctx, bot)
if err != nil {
return 0, err
}
s.invalidateBotReadCaches(ctx, botUserID)
return version, nil
}
func (s *Service) GetAttachMenuBot(ctx context.Context, botUserID int64) (domain.BotAttachMenuBot, bool, error) {
if s == nil || s.bots == nil {
return domain.BotAttachMenuBot{}, false, nil
}
return s.bots.GetAttachMenuBot(ctx, botUserID)
}
func (s *Service) ListAttachMenuBots(ctx context.Context) ([]domain.BotAttachMenuBot, error) {
if s == nil || s.bots == nil {
return nil, nil
}
return s.bots.ListAttachMenuBots(ctx)
}
func (s *Service) GetAttachMenuState(ctx context.Context, userID, botUserID int64) (domain.BotAttachMenuState, bool, error) {
if s == nil || s.bots == nil {
return domain.BotAttachMenuState{}, false, nil
}
return s.bots.GetAttachMenuState(ctx, userID, botUserID)
}
func (s *Service) SetAttachMenuState(ctx context.Context, state domain.BotAttachMenuState) (domain.BotAttachMenuState, error) {
if s == nil || s.bots == nil {
return domain.BotAttachMenuState{}, domain.ErrBotAttachMenuInvalid
}
return s.bots.SetAttachMenuState(ctx, state)
}
func (s *Service) SaveRequestedWebViewButton(ctx context.Context, button domain.BotRequestedWebViewButton) (domain.BotRequestedWebViewButton, error) {
if s == nil || s.bots == nil || button.BotUserID == 0 || button.UserID == 0 || button.ButtonID == 0 {
return domain.BotRequestedWebViewButton{}, domain.ErrBotRequestedButtonInvalid
}
if button.WebAppReqID == "" {
rnd, err := randomInt64()
if err != nil {
return domain.BotRequestedWebViewButton{}, err
}
button.WebAppReqID = hex.EncodeToString([]byte(fmt.Sprintf("%d:%d:%d", button.BotUserID, button.UserID, rnd)))
}
if button.MaxQuantity <= 0 {
button.MaxQuantity = 1
}
if button.MaxQuantity > domain.MaxBotRequestedPeerQuantity {
return domain.BotRequestedWebViewButton{}, domain.ErrBotRequestedButtonInvalid
}
now := s.now()
if button.CreatedAt.IsZero() {
button.CreatedAt = now
}
if button.ExpiresAt.IsZero() {
button.ExpiresAt = now.Add(requestedWebViewButtonTTL)
}
if err := s.bots.SaveRequestedWebViewButton(ctx, button); err != nil {
return domain.BotRequestedWebViewButton{}, err
}
return button, nil
}
func (s *Service) GetRequestedWebViewButton(ctx context.Context, botUserID, userID int64, reqID string) (domain.BotRequestedWebViewButton, bool, error) {
if s == nil || s.bots == nil {
return domain.BotRequestedWebViewButton{}, false, nil
}
return s.bots.GetRequestedWebViewButton(ctx, botUserID, userID, reqID)
}
func (s *Service) DeleteRequestedWebViewButton(ctx context.Context, botUserID, userID int64, reqID string) error {
if s == nil || s.bots == nil {
return nil
}
return s.bots.DeleteRequestedWebViewButton(ctx, botUserID, userID, reqID)
}
func (s *Service) SetBotEmojiStatusPermission(ctx context.Context, botUserID, userID int64, allowed bool) error {
if s == nil || s.bots == nil {
return domain.ErrBotNotFound
}
return s.bots.SetBotEmojiStatusPermission(ctx, botUserID, userID, allowed)
}
func (s *Service) BotEmojiStatusPermission(ctx context.Context, botUserID, userID int64) (bool, error) {
if s == nil || s.bots == nil {
return false, nil
}
return s.bots.BotEmojiStatusPermission(ctx, botUserID, userID)
}
func (s *Service) PutWebViewCustomMethodQuery(ctx context.Context, botUserID, userID int64, method, paramsJSON string) (domain.BotWebViewCustomMethodQuery, error) {
method = strings.TrimSpace(method)
if s == nil || s.bots == nil || botUserID == 0 || userID == 0 || method == "" || len(method) > domain.MaxBotCustomMethodLen || len(paramsJSON) > domain.MaxBotCustomMethodPayloadLen {
return domain.BotWebViewCustomMethodQuery{}, domain.ErrBotCustomMethodUnavailable
}
rnd, err := randomInt64()
if err != nil {
return domain.BotWebViewCustomMethodQuery{}, err
}
now := s.now()
query := domain.BotWebViewCustomMethodQuery{
ID: fmt.Sprintf("%d:%d:%d:%d", botUserID, userID, now.UnixNano(), rnd),
BotUserID: botUserID,
UserID: userID,
CustomMethod: method,
ParamsJSON: paramsJSON,
CreatedAt: now,
ExpiresAt: now.Add(webViewCustomMethodQueryTTL),
}
if err := s.bots.PutWebViewCustomMethodQuery(ctx, query); err != nil {
return domain.BotWebViewCustomMethodQuery{}, err
}
return query, nil
}
func validHTTPSURL(raw string) bool {
u, err := url.Parse(raw)
return err == nil && u.Scheme == "https" && u.Host != ""
}
func validBotAppShortName(shortName string) bool {
if shortName == "" || len(shortName) > domain.MaxBotAppShortNameLen {
return false
}
for _, r := range shortName {
switch {
case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '_':
default:
return false
}
}
return true
}
func botAppHash(app domain.BotApp) int64 {
return stableBotAppInt64("bot-app-hash", fmt.Sprint(app.BotUserID), app.ShortName, app.Title, app.Description, app.URL, fmt.Sprint(app.PhotoID), fmt.Sprint(app.DocumentID), fmt.Sprint(app.Inactive), fmt.Sprint(app.RequestWriteAccess), fmt.Sprint(app.HasSettings), fmt.Sprint(app.Main))
}
func stableBotAppInt64(parts ...string) int64 {
h := sha256.New()
for _, part := range parts {
_, _ = h.Write([]byte(part))
_, _ = h.Write([]byte{0})
}
sum := h.Sum(nil)
value := int64(binary.BigEndian.Uint64(sum[:8]) & 0x7fffffffffffffff)
if value == 0 {
return 1
}
return value
}