810 lines
24 KiB
Go
810 lines
24 KiB
Go
package memory
|
||
|
||
import (
|
||
"context"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// BotStore 是 store.BotStore 的内存实现。bot 的 users 行经注入的 UserStore 创建,
|
||
// 与 postgres 实现(单事务建 users+bots 两行)保持可见性一致。
|
||
// 内置 service bots 的 bots 行预置(token 为空 = 不可登录),对齐迁移种子。
|
||
type BotStore struct {
|
||
mu sync.RWMutex
|
||
users *UserStore
|
||
byID map[int64]domain.BotProfile
|
||
states map[[2]int64]domain.BotChatState
|
||
permissions map[[2]int64]bool
|
||
appsByID map[int64]domain.BotApp
|
||
appShortNames map[botAppShortKey]int64
|
||
appSettings map[int64]domain.BotAppSettings
|
||
previewMedia map[int64]map[int64]domain.BotAppPreviewMedia
|
||
previewSeq int64
|
||
attachMenu map[int64]domain.BotAttachMenuBot
|
||
attachStates map[[2]int64]domain.BotAttachMenuState
|
||
requestButtons map[requestedButtonKey]domain.BotRequestedWebViewButton
|
||
emojiPerms map[[2]int64]bool
|
||
customMethod map[string]domain.BotWebViewCustomMethodQuery
|
||
}
|
||
|
||
type botAppShortKey struct {
|
||
botUserID int64
|
||
shortName string
|
||
}
|
||
|
||
type requestedButtonKey struct {
|
||
botUserID int64
|
||
userID int64
|
||
reqID string
|
||
}
|
||
|
||
// NewBotStore 创建内存 BotStore。
|
||
func NewBotStore(users *UserStore) *BotStore {
|
||
s := &BotStore{
|
||
users: users,
|
||
byID: make(map[int64]domain.BotProfile),
|
||
states: make(map[[2]int64]domain.BotChatState),
|
||
permissions: make(map[[2]int64]bool),
|
||
appsByID: make(map[int64]domain.BotApp),
|
||
appShortNames: make(map[botAppShortKey]int64),
|
||
appSettings: make(map[int64]domain.BotAppSettings),
|
||
previewMedia: make(map[int64]map[int64]domain.BotAppPreviewMedia),
|
||
attachMenu: make(map[int64]domain.BotAttachMenuBot),
|
||
attachStates: make(map[[2]int64]domain.BotAttachMenuState),
|
||
requestButtons: make(map[requestedButtonKey]domain.BotRequestedWebViewButton),
|
||
emojiPerms: make(map[[2]int64]bool),
|
||
customMethod: make(map[string]domain.BotWebViewCustomMethodQuery),
|
||
}
|
||
s.byID[domain.BotFatherUserID] = botFatherSeedProfile()
|
||
s.byID[domain.StickersBotUserID] = stickersSeedProfile()
|
||
s.byID[domain.ChatBotUserID] = chatBotSeedProfile()
|
||
return s
|
||
}
|
||
|
||
func botFatherSeedProfile() domain.BotProfile {
|
||
return domain.BotProfile{
|
||
BotUserID: domain.BotFatherUserID,
|
||
OwnerUserID: domain.BotFatherUserID,
|
||
Description: "BotFather is the one bot to rule them all. Use it to create new bot accounts and manage your existing bots.",
|
||
Commands: []domain.BotCommand{
|
||
{Command: "newbot", Description: "create a new bot"},
|
||
{Command: "mybots", Description: "list your bots"},
|
||
{Command: "token", Description: "show a bot's token"},
|
||
{Command: "revoke", Description: "revoke a bot's token"},
|
||
{Command: "setlogin", Description: "configure Telegram Login"},
|
||
{Command: "logininfo", Description: "show Telegram Login configuration"},
|
||
{Command: "resetloginsecret", Description: "rotate an OIDC Client Secret"},
|
||
{Command: "cancel", Description: "cancel the current operation"},
|
||
{Command: "help", Description: "show help"},
|
||
},
|
||
}
|
||
}
|
||
|
||
func stickersSeedProfile() domain.BotProfile {
|
||
return domain.BotProfile{
|
||
BotUserID: domain.StickersBotUserID,
|
||
OwnerUserID: domain.StickersBotUserID,
|
||
Description: "Create custom sticker and emoji packs for telesrv.",
|
||
Commands: []domain.BotCommand{
|
||
{Command: "start", Description: "start the sticker pack assistant"},
|
||
{Command: "help", Description: "show help"},
|
||
{Command: "newpack", Description: "create a sticker pack"},
|
||
{Command: "newemoji", Description: "create a custom emoji pack"},
|
||
{Command: "addsticker", Description: "add to one of your packs"},
|
||
{Command: "delsticker", Description: "remove one item from a pack"},
|
||
{Command: "publish", Description: "publish the current pack"},
|
||
{Command: "cancel", Description: "cancel the current operation"},
|
||
{Command: "packs", Description: "list your created packs"},
|
||
},
|
||
}
|
||
}
|
||
|
||
func chatBotSeedProfile() domain.BotProfile {
|
||
return domain.BotProfile{
|
||
BotUserID: domain.ChatBotUserID,
|
||
OwnerUserID: domain.ChatBotUserID,
|
||
Description: "Chat with the configured telesrv AI provider.",
|
||
Commands: []domain.BotCommand{
|
||
{Command: "start", Description: "start chatting"},
|
||
{Command: "help", Description: "show help"},
|
||
{Command: "reset", Description: "clear local chat context"},
|
||
},
|
||
}
|
||
}
|
||
|
||
func (s *BotStore) CreateBotAccount(ctx context.Context, user domain.User, profile domain.BotProfile) (domain.User, domain.BotProfile, error) {
|
||
user.Phone = ""
|
||
user.Username = strings.TrimSpace(strings.TrimPrefix(user.Username, "@"))
|
||
user.Bot = true
|
||
if user.BotInfoVersion < 1 {
|
||
user.BotInfoVersion = 1
|
||
}
|
||
// 持 BotStore.mu 跨「复核计数 → 建 users 行 → 写 profile」,与 postgres 的
|
||
// advisory lock + 事务内复核对齐,封死 count-then-insert TOCTOU。锁顺序
|
||
// BotStore.mu → UserStore.mu(UserStore 不反向引用 BotStore,无死锁)。
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
owned := 0
|
||
for _, p := range s.byID {
|
||
if p.OwnerUserID == profile.OwnerUserID && p.BotUserID != p.OwnerUserID {
|
||
owned++
|
||
}
|
||
}
|
||
if owned >= domain.MaxBotsPerOwner {
|
||
return domain.User{}, domain.BotProfile{}, domain.ErrBotsTooMany
|
||
}
|
||
created, err := s.users.Create(ctx, user)
|
||
if err != nil {
|
||
return domain.User{}, domain.BotProfile{}, err
|
||
}
|
||
profile.BotUserID = created.ID
|
||
s.byID[created.ID] = cloneBotProfile(profile)
|
||
return created, profile, nil
|
||
}
|
||
|
||
func (s *BotStore) GetBot(_ context.Context, botUserID int64) (domain.BotProfile, bool, error) {
|
||
s.mu.RLock()
|
||
p, ok := s.byID[botUserID]
|
||
if ok {
|
||
p = s.enrichBotProfileLocked(p)
|
||
}
|
||
s.mu.RUnlock()
|
||
if !ok {
|
||
return domain.BotProfile{}, false, nil
|
||
}
|
||
return cloneBotProfile(p), true, nil
|
||
}
|
||
|
||
func (s *BotStore) GetBots(_ context.Context, botUserIDs []int64) (map[int64]domain.BotProfile, error) {
|
||
if len(botUserIDs) == 0 {
|
||
return nil, nil
|
||
}
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
out := make(map[int64]domain.BotProfile, len(botUserIDs))
|
||
for _, id := range botUserIDs {
|
||
if id == 0 {
|
||
continue
|
||
}
|
||
if _, ok := out[id]; ok {
|
||
continue
|
||
}
|
||
if p, ok := s.byID[id]; ok {
|
||
out[id] = cloneBotProfile(s.enrichBotProfileLocked(p))
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *BotStore) ListBotsByOwner(_ context.Context, ownerUserID int64) ([]domain.BotProfile, error) {
|
||
if ownerUserID == 0 {
|
||
return nil, nil
|
||
}
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
out := make([]domain.BotProfile, 0)
|
||
for _, p := range s.byID {
|
||
if p.OwnerUserID == ownerUserID && p.BotUserID != p.OwnerUserID {
|
||
out = append(out, cloneBotProfile(s.enrichBotProfileLocked(p)))
|
||
}
|
||
}
|
||
sortBotProfiles(out)
|
||
return out, nil
|
||
}
|
||
|
||
func (s *BotStore) CountBotsByOwner(_ context.Context, ownerUserID int64) (int, error) {
|
||
if ownerUserID == 0 {
|
||
return 0, nil
|
||
}
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
n := 0
|
||
for _, p := range s.byID {
|
||
if p.OwnerUserID == ownerUserID && p.BotUserID != p.OwnerUserID {
|
||
n++
|
||
}
|
||
}
|
||
return n, nil
|
||
}
|
||
|
||
func (s *BotStore) UpdateBotTokenSecret(_ context.Context, botUserID int64, secret string) error {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
p, ok := s.byID[botUserID]
|
||
if !ok {
|
||
return domain.ErrBotNotFound
|
||
}
|
||
p.TokenSecret = secret
|
||
s.byID[botUserID] = p
|
||
return nil
|
||
}
|
||
|
||
// editProfile 在 BotStore.mu 内改 bots 行字段,随后 bump users 的 bot_info_version。
|
||
// 与 postgres 不同(非单事务),但 memory 仅测试替身、元数据更新低频,可接受。
|
||
func (s *BotStore) editProfile(botUserID int64, fn func(p *domain.BotProfile)) (int, error) {
|
||
s.mu.Lock()
|
||
p, ok := s.byID[botUserID]
|
||
if !ok {
|
||
s.mu.Unlock()
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
fn(&p)
|
||
s.byID[botUserID] = p
|
||
s.mu.Unlock()
|
||
ver, ok := s.users.bumpBotInfoVersion(botUserID)
|
||
if !ok {
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
return ver, nil
|
||
}
|
||
|
||
func (s *BotStore) UpdateBotCommands(_ context.Context, botUserID int64, commands []domain.BotCommand) (int, error) {
|
||
return s.editProfile(botUserID, func(p *domain.BotProfile) {
|
||
p.Commands = append([]domain.BotCommand(nil), commands...)
|
||
})
|
||
}
|
||
|
||
func (s *BotStore) UpdateBotInfo(_ context.Context, botUserID int64, upd domain.BotInfoUpdate) (int, error) {
|
||
if _, ok := s.GetBotProfile(botUserID); !ok {
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
if upd.SetName || upd.SetAbout {
|
||
if !s.users.updateBotProfile(botUserID, upd.SetName, upd.Name, upd.SetAbout, upd.About) {
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
}
|
||
return s.editProfile(botUserID, func(p *domain.BotProfile) {
|
||
if upd.SetDescription {
|
||
p.Description = upd.Description
|
||
}
|
||
})
|
||
}
|
||
|
||
func (s *BotStore) UpdateBotMenuButton(_ context.Context, botUserID int64, button domain.BotMenuButton) (int, error) {
|
||
return s.editProfile(botUserID, func(p *domain.BotProfile) {
|
||
p.MenuButton = button
|
||
})
|
||
}
|
||
|
||
func (s *BotStore) SetBotInlinePlaceholder(_ context.Context, botUserID int64, placeholder string) (int, error) {
|
||
return s.editProfile(botUserID, func(p *domain.BotProfile) {
|
||
p.InlinePlaceholder = placeholder
|
||
})
|
||
}
|
||
|
||
func (s *BotStore) SetBotInlineGeo(_ context.Context, botUserID int64, inlineGeo bool) (int, error) {
|
||
return s.editProfile(botUserID, func(p *domain.BotProfile) { p.InlineGeo = inlineGeo })
|
||
}
|
||
|
||
func (s *BotStore) SetBotNochats(_ context.Context, botUserID int64, nochats bool) (int, error) {
|
||
return s.editProfile(botUserID, func(p *domain.BotProfile) { p.Nochats = nochats })
|
||
}
|
||
|
||
func (s *BotStore) SetBotChatHistory(_ context.Context, botUserID int64, chatHistory bool) (int, error) {
|
||
return s.editProfile(botUserID, func(p *domain.BotProfile) { p.ChatHistory = chatHistory })
|
||
}
|
||
|
||
func (s *BotStore) CanBotSendMessage(_ context.Context, botUserID, userID int64) (bool, error) {
|
||
if botUserID == 0 || userID == 0 || botUserID == userID {
|
||
return false, nil
|
||
}
|
||
s.mu.RLock()
|
||
allowed := s.permissions[[2]int64{botUserID, userID}]
|
||
s.mu.RUnlock()
|
||
return allowed, nil
|
||
}
|
||
|
||
func (s *BotStore) AllowBotSendMessage(_ context.Context, botUserID, userID int64, _ bool) (bool, error) {
|
||
if botUserID == 0 || userID == 0 || botUserID == userID {
|
||
return false, domain.ErrBotNotFound
|
||
}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
if _, ok := s.byID[botUserID]; !ok {
|
||
return false, domain.ErrBotNotFound
|
||
}
|
||
key := [2]int64{botUserID, userID}
|
||
if s.permissions[key] {
|
||
return false, nil
|
||
}
|
||
s.permissions[key] = true
|
||
return true, nil
|
||
}
|
||
|
||
func (s *BotStore) UpsertBotApp(_ context.Context, app domain.BotApp) (domain.BotApp, int, error) {
|
||
if app.BotUserID == 0 || app.ID == 0 || app.AccessHash == 0 || app.ShortName == "" || app.URL == "" {
|
||
return domain.BotApp{}, 0, domain.ErrBotAppInvalid
|
||
}
|
||
app.ShortName = strings.ToLower(strings.TrimSpace(app.ShortName))
|
||
s.mu.Lock()
|
||
if _, ok := s.byID[app.BotUserID]; !ok {
|
||
s.mu.Unlock()
|
||
return domain.BotApp{}, 0, domain.ErrBotNotFound
|
||
}
|
||
if existing, ok := s.appShortNames[botAppShortKey{botUserID: app.BotUserID, shortName: app.ShortName}]; ok && existing != app.ID {
|
||
s.mu.Unlock()
|
||
return domain.BotApp{}, 0, domain.ErrBotAppInvalid
|
||
}
|
||
if old, ok := s.appsByID[app.ID]; ok && old.BotUserID != app.BotUserID {
|
||
s.mu.Unlock()
|
||
return domain.BotApp{}, 0, domain.ErrBotAppInvalid
|
||
}
|
||
s.appsByID[app.ID] = app
|
||
s.appShortNames[botAppShortKey{botUserID: app.BotUserID, shortName: app.ShortName}] = app.ID
|
||
p := s.byID[app.BotUserID]
|
||
if app.Main {
|
||
for id, item := range s.appsByID {
|
||
if item.BotUserID == app.BotUserID && id != app.ID && item.Main {
|
||
item.Main = false
|
||
s.appsByID[id] = item
|
||
}
|
||
}
|
||
p.HasMainApp = true
|
||
}
|
||
if app.HasSettings {
|
||
p.HasMainApp = p.HasMainApp || app.Main
|
||
}
|
||
s.byID[app.BotUserID] = p
|
||
s.mu.Unlock()
|
||
ver, ok := s.users.bumpBotInfoVersion(app.BotUserID)
|
||
if !ok {
|
||
return domain.BotApp{}, 0, domain.ErrBotNotFound
|
||
}
|
||
return cloneBotApp(app), ver, nil
|
||
}
|
||
|
||
func (s *BotStore) GetBotAppByID(_ context.Context, appID, accessHash int64) (domain.BotApp, bool, error) {
|
||
if appID == 0 || accessHash == 0 {
|
||
return domain.BotApp{}, false, nil
|
||
}
|
||
s.mu.RLock()
|
||
app, ok := s.appsByID[appID]
|
||
s.mu.RUnlock()
|
||
if !ok || app.AccessHash != accessHash {
|
||
return domain.BotApp{}, false, nil
|
||
}
|
||
return cloneBotApp(app), true, nil
|
||
}
|
||
|
||
func (s *BotStore) GetBotAppByShortName(_ context.Context, botUserID int64, shortName string) (domain.BotApp, bool, error) {
|
||
key := botAppShortKey{botUserID: botUserID, shortName: strings.ToLower(strings.TrimSpace(shortName))}
|
||
s.mu.RLock()
|
||
id, ok := s.appShortNames[key]
|
||
var app domain.BotApp
|
||
if ok {
|
||
app, ok = s.appsByID[id]
|
||
}
|
||
s.mu.RUnlock()
|
||
if !ok {
|
||
return domain.BotApp{}, false, nil
|
||
}
|
||
return cloneBotApp(app), true, nil
|
||
}
|
||
|
||
func (s *BotStore) GetMainBotApp(_ context.Context, botUserID int64) (domain.BotApp, bool, error) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
for _, app := range s.appsByID {
|
||
if app.BotUserID == botUserID && app.Main {
|
||
return cloneBotApp(app), true, nil
|
||
}
|
||
}
|
||
return domain.BotApp{}, false, nil
|
||
}
|
||
|
||
func (s *BotStore) ListBotApps(_ context.Context, botUserID int64) ([]domain.BotApp, error) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
out := make([]domain.BotApp, 0)
|
||
for _, app := range s.appsByID {
|
||
if app.BotUserID == botUserID {
|
||
out = append(out, cloneBotApp(app))
|
||
}
|
||
}
|
||
sortBotApps(out)
|
||
return out, nil
|
||
}
|
||
|
||
func (s *BotStore) GetBotAppSettings(_ context.Context, botUserID int64) (domain.BotAppSettings, bool, error) {
|
||
s.mu.RLock()
|
||
settings, ok := s.appSettings[botUserID]
|
||
s.mu.RUnlock()
|
||
if !ok {
|
||
return domain.BotAppSettings{}, false, nil
|
||
}
|
||
return cloneBotAppSettings(settings), true, nil
|
||
}
|
||
|
||
func (s *BotStore) UpsertBotAppSettings(_ context.Context, botUserID int64, settings domain.BotAppSettings) (int, error) {
|
||
s.mu.Lock()
|
||
if _, ok := s.byID[botUserID]; !ok {
|
||
s.mu.Unlock()
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
s.appSettings[botUserID] = cloneBotAppSettings(settings)
|
||
p := s.byID[botUserID]
|
||
c := cloneBotAppSettings(settings)
|
||
p.AppSettings = &c
|
||
s.byID[botUserID] = p
|
||
s.mu.Unlock()
|
||
ver, ok := s.users.bumpBotInfoVersion(botUserID)
|
||
if !ok {
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
return ver, nil
|
||
}
|
||
|
||
func (s *BotStore) ListBotAppPreviewMedia(_ context.Context, botUserID, appID int64) ([]domain.BotAppPreviewMedia, error) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
items := s.previewMedia[appID]
|
||
out := make([]domain.BotAppPreviewMedia, 0, len(items))
|
||
for _, media := range items {
|
||
if media.BotUserID == botUserID {
|
||
out = append(out, media)
|
||
}
|
||
}
|
||
sortBotPreviewMedia(out)
|
||
return out, nil
|
||
}
|
||
|
||
func (s *BotStore) UpsertBotAppPreviewMedia(_ context.Context, media domain.BotAppPreviewMedia) (domain.BotAppPreviewMedia, int, error) {
|
||
if media.BotUserID == 0 || media.AppID == 0 || (media.PhotoID == 0 && media.DocumentID == 0) {
|
||
return domain.BotAppPreviewMedia{}, 0, domain.ErrBotAppInvalid
|
||
}
|
||
s.mu.Lock()
|
||
if app, ok := s.appsByID[media.AppID]; !ok || app.BotUserID != media.BotUserID {
|
||
s.mu.Unlock()
|
||
return domain.BotAppPreviewMedia{}, 0, domain.ErrBotAppInvalid
|
||
}
|
||
if s.previewMedia[media.AppID] == nil {
|
||
s.previewMedia[media.AppID] = make(map[int64]domain.BotAppPreviewMedia)
|
||
}
|
||
if media.ID == 0 {
|
||
s.previewSeq++
|
||
media.ID = s.previewSeq
|
||
}
|
||
if media.Position <= 0 {
|
||
media.Position = len(s.previewMedia[media.AppID]) + 1
|
||
}
|
||
s.previewMedia[media.AppID][media.ID] = media
|
||
p := s.byID[media.BotUserID]
|
||
p.HasPreviewMedias = true
|
||
s.byID[media.BotUserID] = p
|
||
s.mu.Unlock()
|
||
ver, ok := s.users.bumpBotInfoVersion(media.BotUserID)
|
||
if !ok {
|
||
return domain.BotAppPreviewMedia{}, 0, domain.ErrBotNotFound
|
||
}
|
||
return media, ver, nil
|
||
}
|
||
|
||
func (s *BotStore) DeleteBotAppPreviewMedia(_ context.Context, botUserID, appID, mediaID int64) (int, error) {
|
||
s.mu.Lock()
|
||
items := s.previewMedia[appID]
|
||
if mediaID == 0 || items == nil {
|
||
s.mu.Unlock()
|
||
return 0, domain.ErrBotAppInvalid
|
||
}
|
||
media, ok := items[mediaID]
|
||
if !ok || media.BotUserID != botUserID {
|
||
s.mu.Unlock()
|
||
return 0, domain.ErrBotAppInvalid
|
||
}
|
||
delete(items, mediaID)
|
||
has := false
|
||
for _, items := range s.previewMedia {
|
||
for _, media := range items {
|
||
if media.BotUserID == botUserID {
|
||
has = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
p := s.byID[botUserID]
|
||
p.HasPreviewMedias = has
|
||
s.byID[botUserID] = p
|
||
s.mu.Unlock()
|
||
ver, ok := s.users.bumpBotInfoVersion(botUserID)
|
||
if !ok {
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
return ver, nil
|
||
}
|
||
|
||
func (s *BotStore) ReorderBotAppPreviewMedia(_ context.Context, botUserID, appID int64, mediaIDs []int64) (int, error) {
|
||
s.mu.Lock()
|
||
items := s.previewMedia[appID]
|
||
if items == nil {
|
||
s.mu.Unlock()
|
||
return 0, domain.ErrBotAppInvalid
|
||
}
|
||
for pos, id := range mediaIDs {
|
||
media, ok := items[id]
|
||
if !ok || media.BotUserID != botUserID {
|
||
s.mu.Unlock()
|
||
return 0, domain.ErrBotAppInvalid
|
||
}
|
||
media.Position = pos + 1
|
||
items[id] = media
|
||
}
|
||
s.mu.Unlock()
|
||
ver, ok := s.users.bumpBotInfoVersion(botUserID)
|
||
if !ok {
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
return ver, nil
|
||
}
|
||
|
||
func (s *BotStore) UpsertAttachMenuBot(_ context.Context, bot domain.BotAttachMenuBot) (int, error) {
|
||
if bot.BotUserID == 0 || bot.ShortName == "" {
|
||
return 0, domain.ErrBotAttachMenuInvalid
|
||
}
|
||
bot.ShortName = strings.ToLower(strings.TrimSpace(bot.ShortName))
|
||
s.mu.Lock()
|
||
if _, ok := s.byID[bot.BotUserID]; !ok {
|
||
s.mu.Unlock()
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
s.attachMenu[bot.BotUserID] = cloneAttachMenuBot(bot)
|
||
p := s.byID[bot.BotUserID]
|
||
p.HasAttachMenu = true
|
||
s.byID[bot.BotUserID] = p
|
||
s.mu.Unlock()
|
||
ver, ok := s.users.bumpBotInfoVersion(bot.BotUserID)
|
||
if !ok {
|
||
return 0, domain.ErrBotNotFound
|
||
}
|
||
return ver, nil
|
||
}
|
||
|
||
func (s *BotStore) GetAttachMenuBot(_ context.Context, botUserID int64) (domain.BotAttachMenuBot, bool, error) {
|
||
s.mu.RLock()
|
||
bot, ok := s.attachMenu[botUserID]
|
||
s.mu.RUnlock()
|
||
if !ok {
|
||
return domain.BotAttachMenuBot{}, false, nil
|
||
}
|
||
return cloneAttachMenuBot(bot), true, nil
|
||
}
|
||
|
||
func (s *BotStore) ListAttachMenuBots(_ context.Context) ([]domain.BotAttachMenuBot, error) {
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
out := make([]domain.BotAttachMenuBot, 0, len(s.attachMenu))
|
||
for _, bot := range s.attachMenu {
|
||
out = append(out, cloneAttachMenuBot(bot))
|
||
}
|
||
sortAttachMenuBots(out)
|
||
return out, nil
|
||
}
|
||
|
||
func (s *BotStore) GetAttachMenuState(_ context.Context, userID, botUserID int64) (domain.BotAttachMenuState, bool, error) {
|
||
s.mu.RLock()
|
||
state, ok := s.attachStates[[2]int64{userID, botUserID}]
|
||
s.mu.RUnlock()
|
||
if !ok {
|
||
return domain.BotAttachMenuState{}, false, nil
|
||
}
|
||
return state, true, nil
|
||
}
|
||
|
||
func (s *BotStore) SetAttachMenuState(_ context.Context, state domain.BotAttachMenuState) (domain.BotAttachMenuState, error) {
|
||
if state.UserID == 0 || state.BotUserID == 0 || state.UserID == state.BotUserID {
|
||
return domain.BotAttachMenuState{}, domain.ErrBotAttachMenuInvalid
|
||
}
|
||
s.mu.Lock()
|
||
if _, ok := s.attachMenu[state.BotUserID]; !ok {
|
||
s.mu.Unlock()
|
||
return domain.BotAttachMenuState{}, domain.ErrBotAttachMenuInvalid
|
||
}
|
||
s.attachStates[[2]int64{state.UserID, state.BotUserID}] = state
|
||
s.mu.Unlock()
|
||
return state, nil
|
||
}
|
||
|
||
func (s *BotStore) SaveRequestedWebViewButton(_ context.Context, button domain.BotRequestedWebViewButton) error {
|
||
if button.BotUserID == 0 || button.UserID == 0 || button.WebAppReqID == "" || button.ButtonID == 0 {
|
||
return domain.ErrBotRequestedButtonInvalid
|
||
}
|
||
s.mu.Lock()
|
||
s.requestButtons[requestedButtonKey{botUserID: button.BotUserID, userID: button.UserID, reqID: button.WebAppReqID}] = button
|
||
s.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
func (s *BotStore) GetRequestedWebViewButton(_ context.Context, botUserID, userID int64, webAppReqID string) (domain.BotRequestedWebViewButton, bool, error) {
|
||
key := requestedButtonKey{botUserID: botUserID, userID: userID, reqID: webAppReqID}
|
||
s.mu.Lock()
|
||
button, ok := s.requestButtons[key]
|
||
if ok && !button.ExpiresAt.IsZero() && !button.ExpiresAt.After(time.Now()) {
|
||
delete(s.requestButtons, key)
|
||
ok = false
|
||
}
|
||
s.mu.Unlock()
|
||
if !ok {
|
||
return domain.BotRequestedWebViewButton{}, false, nil
|
||
}
|
||
return button, true, nil
|
||
}
|
||
|
||
func (s *BotStore) DeleteRequestedWebViewButton(_ context.Context, botUserID, userID int64, webAppReqID string) error {
|
||
s.mu.Lock()
|
||
delete(s.requestButtons, requestedButtonKey{botUserID: botUserID, userID: userID, reqID: webAppReqID})
|
||
s.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
func (s *BotStore) SetBotEmojiStatusPermission(_ context.Context, botUserID, userID int64, allowed bool) error {
|
||
if botUserID == 0 || userID == 0 || botUserID == userID {
|
||
return domain.ErrBotNotFound
|
||
}
|
||
s.mu.Lock()
|
||
if _, ok := s.byID[botUserID]; !ok {
|
||
s.mu.Unlock()
|
||
return domain.ErrBotNotFound
|
||
}
|
||
if allowed {
|
||
s.emojiPerms[[2]int64{botUserID, userID}] = true
|
||
} else {
|
||
delete(s.emojiPerms, [2]int64{botUserID, userID})
|
||
}
|
||
s.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
func (s *BotStore) BotEmojiStatusPermission(_ context.Context, botUserID, userID int64) (bool, error) {
|
||
s.mu.RLock()
|
||
allowed := s.emojiPerms[[2]int64{botUserID, userID}]
|
||
s.mu.RUnlock()
|
||
return allowed, nil
|
||
}
|
||
|
||
func (s *BotStore) PutWebViewCustomMethodQuery(_ context.Context, query domain.BotWebViewCustomMethodQuery) error {
|
||
if query.ID == "" || query.BotUserID == 0 || query.UserID == 0 || query.CustomMethod == "" {
|
||
return domain.ErrBotCustomMethodUnavailable
|
||
}
|
||
s.mu.Lock()
|
||
s.customMethod[query.ID] = query
|
||
s.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
// GetBotProfile 是 GetBot 的同步只读快照(editProfile 前置存在性检查用)。
|
||
func (s *BotStore) GetBotProfile(botUserID int64) (domain.BotProfile, bool) {
|
||
s.mu.RLock()
|
||
p, ok := s.byID[botUserID]
|
||
s.mu.RUnlock()
|
||
return p, ok
|
||
}
|
||
|
||
func (s *BotStore) GetBotChatState(_ context.Context, botUserID, userID int64) (domain.BotChatState, bool, error) {
|
||
s.mu.RLock()
|
||
state, ok := s.states[[2]int64{botUserID, userID}]
|
||
s.mu.RUnlock()
|
||
if !ok {
|
||
return domain.BotChatState{}, false, nil
|
||
}
|
||
return cloneBotChatState(state), true, nil
|
||
}
|
||
|
||
func (s *BotStore) UpsertBotChatState(_ context.Context, state domain.BotChatState) error {
|
||
s.mu.Lock()
|
||
s.states[[2]int64{state.BotUserID, state.UserID}] = cloneBotChatState(state)
|
||
s.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
func (s *BotStore) DeleteBotChatState(_ context.Context, botUserID, userID int64) error {
|
||
s.mu.Lock()
|
||
delete(s.states, [2]int64{botUserID, userID})
|
||
s.mu.Unlock()
|
||
return nil
|
||
}
|
||
|
||
func cloneBotProfile(p domain.BotProfile) domain.BotProfile {
|
||
out := p
|
||
out.Commands = append([]domain.BotCommand(nil), p.Commands...)
|
||
if p.AppSettings != nil {
|
||
settings := cloneBotAppSettings(*p.AppSettings)
|
||
out.AppSettings = &settings
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (s *BotStore) enrichBotProfileLocked(p domain.BotProfile) domain.BotProfile {
|
||
if _, ok := s.appSettings[p.BotUserID]; ok {
|
||
settings := cloneBotAppSettings(s.appSettings[p.BotUserID])
|
||
p.AppSettings = &settings
|
||
}
|
||
for _, app := range s.appsByID {
|
||
if app.BotUserID == p.BotUserID && app.Main {
|
||
p.HasMainApp = true
|
||
break
|
||
}
|
||
}
|
||
if _, ok := s.attachMenu[p.BotUserID]; ok {
|
||
p.HasAttachMenu = true
|
||
}
|
||
for _, items := range s.previewMedia {
|
||
for _, media := range items {
|
||
if media.BotUserID == p.BotUserID {
|
||
p.HasPreviewMedias = true
|
||
return p
|
||
}
|
||
}
|
||
}
|
||
return p
|
||
}
|
||
|
||
func cloneBotApp(app domain.BotApp) domain.BotApp {
|
||
return app
|
||
}
|
||
|
||
func cloneBotAppSettings(settings domain.BotAppSettings) domain.BotAppSettings {
|
||
out := settings
|
||
out.PlaceholderPath = append([]byte(nil), settings.PlaceholderPath...)
|
||
return out
|
||
}
|
||
|
||
func cloneAttachMenuBot(bot domain.BotAttachMenuBot) domain.BotAttachMenuBot {
|
||
out := bot
|
||
out.PeerTypes = append([]string(nil), bot.PeerTypes...)
|
||
out.Icons = make([]domain.BotAttachMenuIcon, len(bot.Icons))
|
||
for i, icon := range bot.Icons {
|
||
out.Icons[i] = icon
|
||
out.Icons[i].Colors = append([]domain.BotAttachMenuIconColor(nil), icon.Colors...)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func cloneBotChatState(state domain.BotChatState) domain.BotChatState {
|
||
out := state
|
||
if state.Draft != nil {
|
||
out.Draft = make(map[string]string, len(state.Draft))
|
||
for k, v := range state.Draft {
|
||
out.Draft[k] = v
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func sortBotProfiles(list []domain.BotProfile) {
|
||
for i := 1; i < len(list); i++ {
|
||
for j := i; j > 0 && list[j].BotUserID < list[j-1].BotUserID; j-- {
|
||
list[j], list[j-1] = list[j-1], list[j]
|
||
}
|
||
}
|
||
}
|
||
|
||
func sortBotApps(list []domain.BotApp) {
|
||
for i := 1; i < len(list); i++ {
|
||
for j := i; j > 0 && appSortKey(list[j]) < appSortKey(list[j-1]); j-- {
|
||
list[j], list[j-1] = list[j-1], list[j]
|
||
}
|
||
}
|
||
}
|
||
|
||
func appSortKey(app domain.BotApp) string {
|
||
return strconv.FormatInt(app.BotUserID, 10) + ":" + app.ShortName
|
||
}
|
||
|
||
func sortBotPreviewMedia(list []domain.BotAppPreviewMedia) {
|
||
for i := 1; i < len(list); i++ {
|
||
for j := i; j > 0 && (list[j].Position < list[j-1].Position || (list[j].Position == list[j-1].Position && list[j].ID < list[j-1].ID)); j-- {
|
||
list[j], list[j-1] = list[j-1], list[j]
|
||
}
|
||
}
|
||
}
|
||
|
||
func sortAttachMenuBots(list []domain.BotAttachMenuBot) {
|
||
for i := 1; i < len(list); i++ {
|
||
for j := i; j > 0 && list[j].BotUserID < list[j-1].BotUserID; j-- {
|
||
list[j], list[j-1] = list[j-1], list[j]
|
||
}
|
||
}
|
||
}
|