feat: sync AI compose and ChatBot features
This commit is contained in:
parent
35e5d38f4d
commit
b7269b135f
75 changed files with 5426 additions and 123 deletions
208
internal/store/memory/ai.go
Normal file
208
internal/store/memory/ai.go
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// AIComposeStore 是 store.AIComposeStore 的内存实现。
|
||||
type AIComposeStore struct {
|
||||
mu sync.RWMutex
|
||||
byID map[int64]domain.AIComposeTone
|
||||
bySlug map[string]int64
|
||||
saves map[int64]map[int64]int64 // userID -> toneID -> order
|
||||
seq int64
|
||||
}
|
||||
|
||||
func NewAIComposeStore() *AIComposeStore {
|
||||
return &AIComposeStore{
|
||||
byID: make(map[int64]domain.AIComposeTone),
|
||||
bySlug: make(map[string]int64),
|
||||
saves: make(map[int64]map[int64]int64),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AIComposeStore) CreateAIComposeTone(_ context.Context, tone domain.AIComposeTone) error {
|
||||
if tone.ID == 0 || tone.AccessHash == 0 || tone.OwnerUserID == 0 || tone.Slug == "" {
|
||||
return domain.ErrAIComposeToneInvalid
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, ok := s.byID[tone.ID]; ok {
|
||||
return domain.ErrAIComposeToneInvalid
|
||||
}
|
||||
if _, ok := s.bySlug[tone.Slug]; ok {
|
||||
return domain.ErrAIComposeToneInvalid
|
||||
}
|
||||
s.byID[tone.ID] = tone.Clone()
|
||||
s.bySlug[tone.Slug] = tone.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AIComposeStore) UpdateAIComposeTone(_ context.Context, tone domain.AIComposeTone) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
prev, ok := s.byID[tone.ID]
|
||||
if !ok {
|
||||
return domain.ErrAIComposeToneNotFound
|
||||
}
|
||||
if prev.OwnerUserID != tone.OwnerUserID {
|
||||
return domain.ErrAIComposeToneInvalid
|
||||
}
|
||||
if tone.Slug != prev.Slug {
|
||||
if _, taken := s.bySlug[tone.Slug]; taken {
|
||||
return domain.ErrAIComposeToneInvalid
|
||||
}
|
||||
delete(s.bySlug, prev.Slug)
|
||||
s.bySlug[tone.Slug] = tone.ID
|
||||
}
|
||||
s.byID[tone.ID] = tone.Clone()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AIComposeStore) DeleteAIComposeTone(_ context.Context, ownerUserID, toneID int64) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
tone, ok := s.byID[toneID]
|
||||
if !ok {
|
||||
return domain.ErrAIComposeToneNotFound
|
||||
}
|
||||
if tone.OwnerUserID != ownerUserID {
|
||||
return domain.ErrAIComposeToneInvalid
|
||||
}
|
||||
delete(s.byID, toneID)
|
||||
delete(s.bySlug, tone.Slug)
|
||||
for userID := range s.saves {
|
||||
delete(s.saves[userID], toneID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AIComposeStore) GetAIComposeToneByID(_ context.Context, id, accessHash int64) (domain.AIComposeTone, bool, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
tone, ok := s.byID[id]
|
||||
if !ok || tone.AccessHash != accessHash {
|
||||
return domain.AIComposeTone{}, false, nil
|
||||
}
|
||||
return tone.Clone(), true, nil
|
||||
}
|
||||
|
||||
func (s *AIComposeStore) GetAIComposeToneBySlug(_ context.Context, slug string) (domain.AIComposeTone, bool, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
id, ok := s.bySlug[slug]
|
||||
if !ok {
|
||||
return domain.AIComposeTone{}, false, nil
|
||||
}
|
||||
tone, ok := s.byID[id]
|
||||
if !ok {
|
||||
return domain.AIComposeTone{}, false, nil
|
||||
}
|
||||
return tone.Clone(), true, nil
|
||||
}
|
||||
|
||||
func (s *AIComposeStore) ListAIComposeTonesForUser(_ context.Context, userID int64) ([]domain.AIComposeTone, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
seen := make(map[int64]bool)
|
||||
out := make([]domain.AIComposeTone, 0)
|
||||
for _, tone := range s.byID {
|
||||
if tone.OwnerUserID != userID {
|
||||
continue
|
||||
}
|
||||
item := tone.Clone()
|
||||
item.Creator = true
|
||||
item.Saved = true
|
||||
out = append(out, item)
|
||||
seen[item.ID] = true
|
||||
}
|
||||
if saved := s.saves[userID]; len(saved) > 0 {
|
||||
type row struct {
|
||||
tone domain.AIComposeTone
|
||||
order int64
|
||||
}
|
||||
rows := make([]row, 0, len(saved))
|
||||
for id, order := range saved {
|
||||
if seen[id] {
|
||||
continue
|
||||
}
|
||||
tone, ok := s.byID[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tone = tone.Clone()
|
||||
tone.Creator = false
|
||||
tone.Saved = true
|
||||
rows = append(rows, row{tone: tone, order: order})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].order < rows[j].order })
|
||||
for _, row := range rows {
|
||||
out = append(out, row.tone)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
if out[i].Creator != out[j].Creator {
|
||||
return out[i].Creator
|
||||
}
|
||||
if out[i].UpdatedAt != out[j].UpdatedAt {
|
||||
return out[i].UpdatedAt > out[j].UpdatedAt
|
||||
}
|
||||
return out[i].ID < out[j].ID
|
||||
})
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *AIComposeStore) SaveAIComposeTone(_ context.Context, userID, toneID int64) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
tone, ok := s.byID[toneID]
|
||||
if !ok {
|
||||
return domain.ErrAIComposeToneNotFound
|
||||
}
|
||||
if tone.OwnerUserID == userID {
|
||||
return nil
|
||||
}
|
||||
byUser := s.saves[userID]
|
||||
if byUser == nil {
|
||||
byUser = make(map[int64]int64)
|
||||
s.saves[userID] = byUser
|
||||
}
|
||||
if _, ok := byUser[toneID]; ok {
|
||||
return nil
|
||||
}
|
||||
s.seq++
|
||||
byUser[toneID] = s.seq
|
||||
tone.InstallsCount++
|
||||
s.byID[toneID] = tone
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AIComposeStore) UnsaveAIComposeTone(_ context.Context, userID, toneID int64) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if byUser := s.saves[userID]; byUser != nil {
|
||||
delete(byUser, toneID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AIComposeStore) SavedAIComposeToneCount(_ context.Context, userID int64) (int, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
seen := make(map[int64]bool)
|
||||
for _, tone := range s.byID {
|
||||
if tone.OwnerUserID == userID {
|
||||
seen[tone.ID] = true
|
||||
}
|
||||
}
|
||||
for toneID := range s.saves[userID] {
|
||||
if _, ok := s.byID[toneID]; ok {
|
||||
seen[toneID] = true
|
||||
}
|
||||
}
|
||||
return len(seen), nil
|
||||
}
|
||||
|
|
@ -61,6 +61,7 @@ func NewBotStore(users *UserStore) *BotStore {
|
|||
}
|
||||
s.byID[domain.BotFatherUserID] = botFatherSeedProfile()
|
||||
s.byID[domain.StickersBotUserID] = stickersSeedProfile()
|
||||
s.byID[domain.ChatBotUserID] = chatBotSeedProfile()
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +100,19 @@ func stickersSeedProfile() domain.BotProfile {
|
|||
}
|
||||
}
|
||||
|
||||
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, "@"))
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ func (s *MessageStore) EditMessage(_ context.Context, req domain.EditMessageRequ
|
|||
if req.Message == "" && req.Media == nil && target.Media.IsZero() {
|
||||
return res, domain.ErrMessageEmpty
|
||||
}
|
||||
if req.Media == nil && !req.SetReplyMarkup && target.Body == req.Message && equalMessageEntities(target.Entities, req.Entities) {
|
||||
if req.Media == nil && !req.SetReplyMarkup && target.Body == req.Message && target.HideEdited == req.HideEdited && equalMessageEntities(target.Entities, req.Entities) {
|
||||
return res, domain.ErrMessageNotModified
|
||||
}
|
||||
messageSenderID := target.From.ID
|
||||
|
|
@ -74,6 +74,7 @@ func (s *MessageStore) EditMessage(_ context.Context, req domain.EditMessageRequ
|
|||
msg.ReplyMarkup = cloneReplyMarkup(req.ReplyMarkup)
|
||||
}
|
||||
msg.EditDate = req.EditDate
|
||||
msg.HideEdited = req.HideEdited
|
||||
msg.Pts = s.nextPtsLocked(userID)
|
||||
s.m[userID][i] = msg
|
||||
event := editMessageEvent(msg)
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ type UserStore struct {
|
|||
nextID int64
|
||||
}
|
||||
|
||||
// NewUserStore 创建内存 UserStore。内置系统账号(777000 / BotFather / Stickers)
|
||||
// NewUserStore 创建内存 UserStore。内置系统账号(777000 / BotFather / Stickers / ChatBot)
|
||||
// 预置进表,与 postgres 的迁移种子保持双 store 行为一致。
|
||||
func NewUserStore() *UserStore {
|
||||
s := &UserStore{byID: make(map[int64]domain.User), nextID: domain.UserIDSequenceBase}
|
||||
for _, id := range []int64{domain.OfficialSystemUserID, domain.BotFatherUserID, domain.StickersBotUserID} {
|
||||
for _, id := range []int64{domain.OfficialSystemUserID, domain.BotFatherUserID, domain.StickersBotUserID, domain.ChatBotUserID} {
|
||||
if u, ok := domain.SystemUserByID(id); ok {
|
||||
s.byID[u.ID] = u
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue