package memory import ( "context" "hash/fnv" "sort" "strings" "telesrv/internal/domain" ) func (s *PasswordStore) GetBusinessProfile(_ context.Context, userID int64) (domain.BusinessProfile, bool, error) { s.mu.RLock() profile, ok := s.businessProfiles[userID] s.mu.RUnlock() return cloneBusinessProfile(profile), ok, nil } func (s *PasswordStore) SaveBusinessProfile(_ context.Context, profile domain.BusinessProfile) error { if profile.UserID == 0 { return domain.ErrBusinessProfileInvalid } s.mu.Lock() s.businessProfiles[profile.UserID] = cloneBusinessProfile(profile) s.mu.Unlock() return nil } func (s *PasswordStore) ListBusinessChatLinks(_ context.Context, ownerUserID int64) ([]domain.BusinessChatLink, error) { s.mu.RLock() slugs := append([]string(nil), s.businessChatLinkSlugs[ownerUserID]...) out := make([]domain.BusinessChatLink, 0, len(slugs)) for _, slug := range slugs { if link, ok := s.businessChatLinks[slug]; ok { out = append(out, cloneBusinessChatLink(link)) } } s.mu.RUnlock() sort.Slice(out, func(i, j int) bool { if out[i].CreatedAt == out[j].CreatedAt { return out[i].Slug < out[j].Slug } return out[i].CreatedAt < out[j].CreatedAt }) return out, nil } func (s *PasswordStore) CreateBusinessChatLink(_ context.Context, link domain.BusinessChatLink) (domain.BusinessChatLink, error) { if link.OwnerUserID == 0 || link.Slug == "" || link.Link == "" || link.Message == "" { return domain.BusinessChatLink{}, domain.ErrBusinessChatLinkInvalid } s.mu.Lock() defer s.mu.Unlock() if _, exists := s.businessChatLinks[link.Slug]; exists { return domain.BusinessChatLink{}, domain.ErrBusinessChatLinkInvalid } if len(s.businessChatLinkSlugs[link.OwnerUserID]) >= domain.MaxBusinessChatLinks { return domain.BusinessChatLink{}, domain.ErrBusinessChatLinksTooMuch } link = cloneBusinessChatLink(link) s.businessChatLinks[link.Slug] = link s.businessChatLinkSlugs[link.OwnerUserID] = append(s.businessChatLinkSlugs[link.OwnerUserID], link.Slug) return cloneBusinessChatLink(link), nil } func (s *PasswordStore) UpdateBusinessChatLink(_ context.Context, ownerUserID int64, slug string, input domain.BusinessChatLinkInput) (domain.BusinessChatLink, error) { s.mu.Lock() defer s.mu.Unlock() link, ok := s.businessChatLinks[slug] if !ok || link.OwnerUserID != ownerUserID { return domain.BusinessChatLink{}, domain.ErrBusinessChatLinkNotFound } link.Message = input.Message link.Entities = append([]domain.MessageEntity(nil), input.Entities...) link.Title = input.Title s.businessChatLinks[slug] = cloneBusinessChatLink(link) return cloneBusinessChatLink(link), nil } func (s *PasswordStore) DeleteBusinessChatLink(_ context.Context, ownerUserID int64, slug string) (bool, error) { s.mu.Lock() defer s.mu.Unlock() link, ok := s.businessChatLinks[slug] if !ok || link.OwnerUserID != ownerUserID { return false, nil } delete(s.businessChatLinks, slug) slugs := s.businessChatLinkSlugs[ownerUserID] next := slugs[:0] for _, item := range slugs { if item != slug { next = append(next, item) } } s.businessChatLinkSlugs[ownerUserID] = next return true, nil } func (s *PasswordStore) ResolveBusinessChatLink(_ context.Context, slug string, bumpViews bool) (domain.BusinessChatLink, bool, error) { s.mu.Lock() defer s.mu.Unlock() link, ok := s.businessChatLinks[slug] if !ok { return domain.BusinessChatLink{}, false, nil } if bumpViews { link.Views++ s.businessChatLinks[slug] = link } return cloneBusinessChatLink(link), true, nil } func (s *PasswordStore) ListQuickReplies(_ context.Context, ownerUserID int64, includeTopMessages bool) (domain.QuickReplyList, error) { s.mu.RLock() out := s.quickReplyListLocked(ownerUserID, includeTopMessages) s.mu.RUnlock() return out, nil } func (s *PasswordStore) CheckQuickReplyShortcut(_ context.Context, ownerUserID int64, shortcut string) (bool, error) { shortcut, err := domain.NormalizeQuickReplyShortcut(shortcut) if err != nil { return false, err } s.mu.RLock() _, ok := s.quickReplyByShortcut[ownerUserID][quickReplyShortcutKey(shortcut)] s.mu.RUnlock() return !ok, nil } func (s *PasswordStore) SaveQuickReplyText(_ context.Context, ownerUserID int64, shortcut string, msg domain.QuickReplyMessage) (domain.QuickReplyMutation, error) { shortcut, err := domain.NormalizeQuickReplyShortcut(shortcut) if err != nil { return domain.QuickReplyMutation{}, err } if msg.Message == "" || len(msg.Entities) > domain.MaxMessageEntityCount { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } s.mu.Lock() defer s.mu.Unlock() s.ensureQuickReplyMapsLocked(ownerUserID) key := quickReplyShortcutKey(shortcut) replyID, found := s.quickReplyByShortcut[ownerUserID][key] created := false if !found { if len(s.quickReplies[ownerUserID]) >= domain.MaxQuickReplies { return domain.QuickReplyMutation{}, domain.ErrQuickRepliesTooMuch } replyID = s.nextQuickReplyID[ownerUserID] + 1 s.nextQuickReplyID[ownerUserID] = replyID s.quickReplyByShortcut[ownerUserID][key] = replyID s.quickReplies[ownerUserID][replyID] = domain.QuickReply{ OwnerUserID: ownerUserID, ID: replyID, Shortcut: shortcut, SortOrder: len(s.quickReplies[ownerUserID]) + 1, } s.quickReplyMessages[ownerUserID][replyID] = make(map[int]domain.QuickReplyMessage) created = true } if len(s.quickReplyMessages[ownerUserID][replyID]) >= domain.MaxQuickReplyMessages { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } msg.OwnerUserID = ownerUserID msg.ShortcutID = replyID msg.ID = s.nextQuickReplyMessageID[ownerUserID] + 1 s.nextQuickReplyMessageID[ownerUserID] = msg.ID msg.Entities = append([]domain.MessageEntity(nil), msg.Entities...) s.quickReplyMessages[ownerUserID][replyID][msg.ID] = msg s.refreshQuickReplyLocked(ownerUserID, replyID) reply := cloneQuickReply(s.quickReplies[ownerUserID][replyID]) kind := domain.QuickReplyMutationMessage if created { kind = domain.QuickReplyMutationNew } return domain.QuickReplyMutation{ Kind: kind, List: s.quickReplyListLocked(ownerUserID, true), QuickReply: reply, ShortcutID: replyID, Message: cloneQuickReplyMessage(msg), }, nil } func (s *PasswordStore) GetQuickReplyMessages(_ context.Context, ownerUserID int64, shortcutID int, ids []int) (domain.QuickReplyMessages, error) { s.mu.RLock() defer s.mu.RUnlock() if _, ok := s.quickReplies[ownerUserID][shortcutID]; !ok { return domain.QuickReplyMessages{}, domain.ErrShortcutInvalid } msgs := s.quickReplyMessages[ownerUserID][shortcutID] selected := make([]domain.QuickReplyMessage, 0, len(msgs)) if len(ids) == 0 { for _, msg := range msgs { selected = append(selected, cloneQuickReplyMessage(msg)) } } else { for _, id := range ids { msg, ok := msgs[id] if !ok { return domain.QuickReplyMessages{}, domain.ErrShortcutInvalid } selected = append(selected, cloneQuickReplyMessage(msg)) } } sortQuickReplyMessages(selected) return domain.QuickReplyMessages{ OwnerUserID: ownerUserID, ShortcutID: shortcutID, Messages: selected, Count: len(msgs), Hash: quickReplyMessagesHash(selected), }, nil } func (s *PasswordStore) RenameQuickReplyShortcut(_ context.Context, ownerUserID int64, shortcutID int, shortcut string) (domain.QuickReplyMutation, error) { shortcut, err := domain.NormalizeQuickReplyShortcut(shortcut) if err != nil { return domain.QuickReplyMutation{}, err } s.mu.Lock() defer s.mu.Unlock() reply, ok := s.quickReplies[ownerUserID][shortcutID] if !ok { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } key := quickReplyShortcutKey(shortcut) if existingID, exists := s.quickReplyByShortcut[ownerUserID][key]; exists && existingID != shortcutID { return domain.QuickReplyMutation{}, domain.ErrShortcutOccupied } delete(s.quickReplyByShortcut[ownerUserID], quickReplyShortcutKey(reply.Shortcut)) reply.Shortcut = shortcut s.quickReplyByShortcut[ownerUserID][key] = shortcutID s.quickReplies[ownerUserID][shortcutID] = reply return domain.QuickReplyMutation{Kind: domain.QuickReplyMutationList, List: s.quickReplyListLocked(ownerUserID, true)}, nil } func (s *PasswordStore) ReorderQuickReplies(_ context.Context, ownerUserID int64, order []int) (domain.QuickReplyMutation, error) { s.mu.Lock() defer s.mu.Unlock() replies := s.quickReplies[ownerUserID] if len(order) != len(replies) { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } seen := make(map[int]struct{}, len(order)) for i, id := range order { reply, ok := replies[id] if !ok { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } if _, dup := seen[id]; dup { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } seen[id] = struct{}{} reply.SortOrder = i + 1 replies[id] = reply } return domain.QuickReplyMutation{Kind: domain.QuickReplyMutationList, List: s.quickReplyListLocked(ownerUserID, true)}, nil } func (s *PasswordStore) DeleteQuickReplyShortcut(_ context.Context, ownerUserID int64, shortcutID int) (domain.QuickReplyMutation, error) { s.mu.Lock() defer s.mu.Unlock() reply, ok := s.quickReplies[ownerUserID][shortcutID] if !ok { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } delete(s.quickReplyByShortcut[ownerUserID], quickReplyShortcutKey(reply.Shortcut)) delete(s.quickReplies[ownerUserID], shortcutID) delete(s.quickReplyMessages[ownerUserID], shortcutID) normalizeQuickReplyOrderLocked(s.quickReplies[ownerUserID]) return domain.QuickReplyMutation{ Kind: domain.QuickReplyMutationDelete, List: s.quickReplyListLocked(ownerUserID, true), ShortcutID: shortcutID, }, nil } func (s *PasswordStore) DeleteQuickReplyMessages(_ context.Context, ownerUserID int64, shortcutID int, ids []int) (domain.QuickReplyMutation, error) { if len(ids) == 0 { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } s.mu.Lock() defer s.mu.Unlock() if _, ok := s.quickReplies[ownerUserID][shortcutID]; !ok { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } msgs := s.quickReplyMessages[ownerUserID][shortcutID] deleted := make([]int, 0, len(ids)) for _, id := range ids { if _, ok := msgs[id]; !ok { return domain.QuickReplyMutation{}, domain.ErrShortcutInvalid } delete(msgs, id) deleted = append(deleted, id) } s.refreshQuickReplyLocked(ownerUserID, shortcutID) return domain.QuickReplyMutation{ Kind: domain.QuickReplyMutationIDs, List: s.quickReplyListLocked(ownerUserID, true), ShortcutID: shortcutID, MessageIDs: deleted, }, nil } func (s *PasswordStore) ReserveBusinessAutomationDelivery(_ context.Context, delivery domain.BusinessAutomationDelivery) (bool, error) { if delivery.OwnerUserID == 0 || delivery.PeerUserID == 0 || delivery.Kind == "" || delivery.TriggerMessageID == 0 { return false, domain.ErrBusinessProfileInvalid } key := businessAutomationDeliveryKey{ ownerUserID: delivery.OwnerUserID, peerUserID: delivery.PeerUserID, kind: delivery.Kind, triggerMessageID: delivery.TriggerMessageID, } s.mu.Lock() defer s.mu.Unlock() if _, ok := s.businessDeliveries[key]; ok { return false, nil } s.businessDeliveries[key] = delivery return true, nil } func (s *PasswordStore) LastBusinessAutomationDelivery(_ context.Context, ownerUserID, peerUserID int64, kind domain.BusinessAutomationKind) (domain.BusinessAutomationDelivery, bool, error) { s.mu.RLock() defer s.mu.RUnlock() var out domain.BusinessAutomationDelivery found := false for _, delivery := range s.businessDeliveries { if delivery.OwnerUserID != ownerUserID || delivery.PeerUserID != peerUserID || delivery.Kind != kind { continue } if !found || delivery.SentAt > out.SentAt || delivery.SentAt == out.SentAt && delivery.TriggerMessageID > out.TriggerMessageID { out = delivery found = true } } return out, found, nil } func (s *PasswordStore) GetConnectedBusinessBot(_ context.Context, ownerUserID int64) (domain.ConnectedBusinessBot, bool, error) { s.mu.RLock() bot, ok := s.connectedBusinessBots[ownerUserID] s.mu.RUnlock() return cloneConnectedBusinessBot(bot), ok, nil } func (s *PasswordStore) SaveConnectedBusinessBot(_ context.Context, bot domain.ConnectedBusinessBot) (domain.ConnectedBusinessBot, error) { if bot.OwnerUserID == 0 || bot.BotUserID == 0 { return domain.ConnectedBusinessBot{}, domain.ErrBotBusinessMissing } s.mu.Lock() if prev, ok := s.connectedBusinessBots[bot.OwnerUserID]; ok && bot.CreatedAtUnix == 0 { bot.CreatedAtUnix = prev.CreatedAtUnix } s.connectedBusinessBots[bot.OwnerUserID] = cloneConnectedBusinessBot(bot) s.mu.Unlock() return cloneConnectedBusinessBot(bot), nil } func (s *PasswordStore) DeleteConnectedBusinessBot(_ context.Context, ownerUserID, botUserID int64) (bool, error) { s.mu.Lock() defer s.mu.Unlock() bot, ok := s.connectedBusinessBots[ownerUserID] if !ok || bot.BotUserID != botUserID { return false, nil } delete(s.connectedBusinessBots, ownerUserID) for key := range s.connectedBusinessBotPeerStates { if key.ownerUserID == ownerUserID { delete(s.connectedBusinessBotPeerStates, key) } } return true, nil } func (s *PasswordStore) SetConnectedBusinessBotPaused(_ context.Context, ownerUserID, peerUserID int64, paused bool) (domain.ConnectedBusinessBotPeerState, error) { if ownerUserID == 0 || peerUserID == 0 { return domain.ConnectedBusinessBotPeerState{}, domain.ErrBotBusinessMissing } key := connectedBusinessBotPeerKey{ownerUserID: ownerUserID, peerUserID: peerUserID} s.mu.Lock() state := s.connectedBusinessBotPeerStates[key] state.OwnerUserID = ownerUserID state.PeerUserID = peerUserID state.Paused = paused s.connectedBusinessBotPeerStates[key] = state s.mu.Unlock() return state, nil } func (s *PasswordStore) DisableConnectedBusinessBotForPeer(_ context.Context, ownerUserID, peerUserID int64) (domain.ConnectedBusinessBotPeerState, error) { if ownerUserID == 0 || peerUserID == 0 { return domain.ConnectedBusinessBotPeerState{}, domain.ErrBotBusinessMissing } key := connectedBusinessBotPeerKey{ownerUserID: ownerUserID, peerUserID: peerUserID} s.mu.Lock() state := s.connectedBusinessBotPeerStates[key] state.OwnerUserID = ownerUserID state.PeerUserID = peerUserID state.Paused = false state.Disabled = true s.connectedBusinessBotPeerStates[key] = state s.mu.Unlock() return state, nil } func (s *PasswordStore) GetConnectedBusinessBotPeerState(_ context.Context, ownerUserID, peerUserID int64) (domain.ConnectedBusinessBotPeerState, bool, error) { key := connectedBusinessBotPeerKey{ownerUserID: ownerUserID, peerUserID: peerUserID} s.mu.RLock() state, ok := s.connectedBusinessBotPeerStates[key] s.mu.RUnlock() return state, ok, nil } func (s *PasswordStore) ensureQuickReplyMapsLocked(ownerUserID int64) { if s.quickReplies[ownerUserID] == nil { s.quickReplies[ownerUserID] = make(map[int]domain.QuickReply) } if s.quickReplyByShortcut[ownerUserID] == nil { s.quickReplyByShortcut[ownerUserID] = make(map[string]int) } if s.quickReplyMessages[ownerUserID] == nil { s.quickReplyMessages[ownerUserID] = make(map[int]map[int]domain.QuickReplyMessage) } for id := range s.quickReplies[ownerUserID] { if s.quickReplyMessages[ownerUserID][id] == nil { s.quickReplyMessages[ownerUserID][id] = make(map[int]domain.QuickReplyMessage) } } } func (s *PasswordStore) refreshQuickReplyLocked(ownerUserID int64, shortcutID int) { s.ensureQuickReplyMapsLocked(ownerUserID) reply := s.quickReplies[ownerUserID][shortcutID] reply.Count = len(s.quickReplyMessages[ownerUserID][shortcutID]) reply.TopMessage = 0 for id := range s.quickReplyMessages[ownerUserID][shortcutID] { if id > reply.TopMessage { reply.TopMessage = id } } s.quickReplies[ownerUserID][shortcutID] = reply } func (s *PasswordStore) quickReplyListLocked(ownerUserID int64, includeTopMessages bool) domain.QuickReplyList { replies := make([]domain.QuickReply, 0, len(s.quickReplies[ownerUserID])) for id := range s.quickReplies[ownerUserID] { s.refreshQuickReplyLocked(ownerUserID, id) replies = append(replies, cloneQuickReply(s.quickReplies[ownerUserID][id])) } sortQuickReplies(replies) messages := make([]domain.QuickReplyMessage, 0, len(replies)) if includeTopMessages { for _, reply := range replies { if reply.TopMessage == 0 { continue } if msg, ok := s.quickReplyMessages[ownerUserID][reply.ID][reply.TopMessage]; ok { messages = append(messages, cloneQuickReplyMessage(msg)) } } sortQuickReplyMessages(messages) } return domain.QuickReplyList{ OwnerUserID: ownerUserID, QuickReplies: replies, Messages: messages, Hash: quickReplyListHash(replies), } } func normalizeQuickReplyOrderLocked(replies map[int]domain.QuickReply) { list := make([]domain.QuickReply, 0, len(replies)) for _, reply := range replies { list = append(list, reply) } sortQuickReplies(list) for i, reply := range list { reply.SortOrder = i + 1 replies[reply.ID] = reply } } func sortQuickReplies(items []domain.QuickReply) { sort.Slice(items, func(i, j int) bool { if items[i].SortOrder == items[j].SortOrder { return items[i].ID < items[j].ID } return items[i].SortOrder < items[j].SortOrder }) } func sortQuickReplyMessages(items []domain.QuickReplyMessage) { sort.Slice(items, func(i, j int) bool { return items[i].ID < items[j].ID }) } func quickReplyShortcutKey(shortcut string) string { return strings.ToLower(shortcut) } func quickReplyListHash(items []domain.QuickReply) int64 { h := fnv.New64a() for _, item := range items { writeHashInt(h, item.ID) writeHashString(h, item.Shortcut) writeHashInt(h, item.TopMessage) writeHashInt(h, item.Count) } return int64(h.Sum64() & 0x7fffffffffffffff) } func quickReplyMessagesHash(items []domain.QuickReplyMessage) int64 { h := fnv.New64a() for _, item := range items { writeHashInt(h, item.ID) writeHashString(h, item.Message) writeHashInt(h, item.Date) } return int64(h.Sum64() & 0x7fffffffffffffff) } func writeHashInt(h interface{ Write([]byte) (int, error) }, v int) { _, _ = h.Write([]byte{ byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v), }) } func writeHashString(h interface{ Write([]byte) (int, error) }, v string) { _, _ = h.Write([]byte(v)) _, _ = h.Write([]byte{0}) } func cloneBusinessProfile(in domain.BusinessProfile) domain.BusinessProfile { out := in if in.WorkHours != nil { v := *in.WorkHours v.WeeklyOpen = append([]domain.BusinessWeeklyOpen(nil), in.WorkHours.WeeklyOpen...) out.WorkHours = &v } if in.Location != nil { v := *in.Location if in.Location.Geo != nil { geo := *in.Location.Geo v.Geo = &geo } out.Location = &v } if in.Intro != nil { v := *in.Intro out.Intro = &v } if in.Greeting != nil { v := *in.Greeting v.Recipients.Users = append([]int64(nil), in.Greeting.Recipients.Users...) out.Greeting = &v } if in.Away != nil { v := *in.Away v.Recipients.Users = append([]int64(nil), in.Away.Recipients.Users...) out.Away = &v } return out } func cloneBusinessChatLink(in domain.BusinessChatLink) domain.BusinessChatLink { out := in out.Entities = append([]domain.MessageEntity(nil), in.Entities...) return out } func cloneQuickReply(in domain.QuickReply) domain.QuickReply { return in } func cloneQuickReplyMessage(in domain.QuickReplyMessage) domain.QuickReplyMessage { out := in out.Entities = append([]domain.MessageEntity(nil), in.Entities...) return out } func cloneConnectedBusinessBot(in domain.ConnectedBusinessBot) domain.ConnectedBusinessBot { out := in out.Recipients.Users = append([]int64(nil), in.Recipients.Users...) out.Recipients.ExcludeUsers = append([]int64(nil), in.Recipients.ExcludeUsers...) return out }