Ctrl K
ComunidadGuías…

internal/app/bots/miniapp.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
}