373 lines
12 KiB
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
|
|
}
|