owpengram-server/internal/store/memory/bot.go

811 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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: "done", Description: "finish Telegram Login configuration"},
{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.muUserStore 不反向引用 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]
}
}
}