owpengram-server/internal/store/memory/memory.go
A 8ff3343ae0 business: add privacy-aware user projection
(cherry picked from commit a636192ef22ee69d792b0ca7db1c6be963be9cb2)
2026-06-14 00:19:59 +08:00

3120 lines
86 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 提供 store 各接口的内存实现,用作测试替身与本地开发兜底。
//
// 与 store/postgres、store/redisstore 对称store 主包只定义接口与 DTO
// 三种后端实现各自独立成包。
package memory
import (
"context"
"encoding/binary"
"fmt"
"hash"
"hash/fnv"
"sort"
"strings"
"sync"
"time"
"telesrv/internal/domain"
"telesrv/internal/store"
)
// AuthKeyStore 是 store.AuthKeyStore 的内存实现。
type AuthKeyStore struct {
mu sync.RWMutex
keys map[[8]byte]store.AuthKeyData
}
// NewAuthKeyStore 创建内存 AuthKeyStore。
func NewAuthKeyStore() *AuthKeyStore {
return &AuthKeyStore{keys: make(map[[8]byte]store.AuthKeyData)}
}
func (s *AuthKeyStore) Save(_ context.Context, k store.AuthKeyData) error {
s.mu.Lock()
s.keys[k.ID] = k
s.mu.Unlock()
return nil
}
func (s *AuthKeyStore) Get(_ context.Context, id [8]byte) (store.AuthKeyData, bool, error) {
s.mu.RLock()
k, ok := s.keys[id]
s.mu.RUnlock()
return k, ok, nil
}
// SessionStore 是 store.SessionStore 的内存实现。
type SessionStore struct {
mu sync.RWMutex
sessions map[int64]store.SessionData
}
// NewSessionStore 创建内存 SessionStore。
func NewSessionStore() *SessionStore {
return &SessionStore{sessions: make(map[int64]store.SessionData)}
}
func (s *SessionStore) Save(_ context.Context, d store.SessionData) error {
s.mu.Lock()
s.sessions[d.ID] = d
s.mu.Unlock()
return nil
}
func (s *SessionStore) Get(_ context.Context, id int64) (store.SessionData, bool, error) {
s.mu.RLock()
d, ok := s.sessions[id]
s.mu.RUnlock()
return d, ok, nil
}
func (s *SessionStore) Delete(_ context.Context, id int64) error {
s.mu.Lock()
delete(s.sessions, id)
s.mu.Unlock()
return nil
}
// UpdateStateStore 是 store.UpdateStateStore 的内存实现。
type UpdateStateStore struct {
mu sync.RWMutex
states map[updateStateKey]domain.UpdateState
}
// UpdateEventStore 是 store.UpdateEventStore 的内存实现。
type UpdateEventStore struct {
mu sync.RWMutex
events map[int64][]domain.UpdateEvent
}
type updateStateKey struct {
authKeyID [8]byte
userID int64
}
// NewUpdateEventStore 创建内存 UpdateEventStore。
func NewUpdateEventStore() *UpdateEventStore {
return &UpdateEventStore{events: make(map[int64][]domain.UpdateEvent)}
}
func (s *UpdateEventStore) Append(_ context.Context, userID int64, event domain.UpdateEvent) error {
event.UserID = userID
event.Message = cloneMessage(event.Message)
event.MessageIDs = append([]int(nil), event.MessageIDs...)
event.Peers = append([]domain.Peer(nil), event.Peers...)
event.Users = append([]domain.User(nil), event.Users...)
event.Channels = append([]domain.Channel(nil), event.Channels...)
s.mu.Lock()
s.events[userID] = append(s.events[userID], event)
s.mu.Unlock()
return nil
}
func (s *UpdateEventStore) ListAfter(_ context.Context, userID int64, pts, limit int) ([]domain.UpdateEvent, error) {
s.mu.RLock()
items := append([]domain.UpdateEvent(nil), s.events[userID]...)
s.mu.RUnlock()
out := make([]domain.UpdateEvent, 0, len(items))
for _, event := range items {
if event.Pts <= pts {
continue
}
event.Message = cloneMessage(event.Message)
event.MessageIDs = append([]int(nil), event.MessageIDs...)
event.Peers = append([]domain.Peer(nil), event.Peers...)
event.Users = append([]domain.User(nil), event.Users...)
event.Channels = append([]domain.Channel(nil), event.Channels...)
out = append(out, event)
if limit > 0 && len(out) >= limit {
break
}
}
return out, nil
}
func (s *UpdateEventStore) Current(_ context.Context, userID int64) (int, error) {
s.mu.RLock()
items := s.events[userID]
s.mu.RUnlock()
max := 0
for _, event := range items {
if event.Pts > max {
max = event.Pts
}
}
return max, nil
}
// MaxContiguousPts 返回从 1 起无空洞的最大 pts内存版按 pts_count 连续扫描)。
func (s *UpdateEventStore) MaxContiguousPts(_ context.Context, userID int64) (int, error) {
s.mu.RLock()
nextByStart := make(map[int]int, len(s.events[userID]))
for _, event := range s.events[userID] {
count := event.PtsCount
if count <= 0 {
count = 1
}
nextByStart[event.Pts-count] = event.Pts
}
s.mu.RUnlock()
contiguous := 0
for {
next, ok := nextByStart[contiguous]
if !ok {
break
}
contiguous = next
}
return contiguous, nil
}
func (s *UpdateEventStore) AdvanceContiguousPts(ctx context.Context, userID int64) (int, error) {
return s.MaxContiguousPts(ctx, userID)
}
// NewUpdateStateStore 创建内存 UpdateStateStore。
func NewUpdateStateStore() *UpdateStateStore {
return &UpdateStateStore{states: make(map[updateStateKey]domain.UpdateState)}
}
func (s *UpdateStateStore) Get(_ context.Context, id [8]byte, userID int64) (domain.UpdateState, bool, error) {
s.mu.RLock()
st, ok := s.states[updateStateKey{authKeyID: id, userID: userID}]
s.mu.RUnlock()
return st, ok, nil
}
func (s *UpdateStateStore) Save(_ context.Context, id [8]byte, userID int64, st domain.UpdateState) error {
s.mu.Lock()
s.states[updateStateKey{authKeyID: id, userID: userID}] = st
s.mu.Unlock()
return nil
}
func (s *UpdateStateStore) Delete(_ context.Context, id [8]byte, userID int64) error {
s.mu.Lock()
delete(s.states, updateStateKey{authKeyID: id, userID: userID})
s.mu.Unlock()
return nil
}
func (s *UpdateStateStore) DeleteAuthKey(_ context.Context, id [8]byte) error {
s.mu.Lock()
for k := range s.states {
if k.authKeyID == id {
delete(s.states, k)
}
}
s.mu.Unlock()
return nil
}
// TempAuthKeyBindingStore 是 store.TempAuthKeyBindingStore 的内存实现。
type TempAuthKeyBindingStore struct {
mu sync.RWMutex
m map[[8]byte]domain.TempAuthKeyBinding
}
// NewTempAuthKeyBindingStore 创建内存 TempAuthKeyBindingStore。
func NewTempAuthKeyBindingStore() *TempAuthKeyBindingStore {
return &TempAuthKeyBindingStore{m: make(map[[8]byte]domain.TempAuthKeyBinding)}
}
func (s *TempAuthKeyBindingStore) Save(_ context.Context, b domain.TempAuthKeyBinding) error {
b.EncryptedMessage = append([]byte(nil), b.EncryptedMessage...)
s.mu.Lock()
s.m[b.TempAuthKeyID] = b
s.mu.Unlock()
return nil
}
func (s *TempAuthKeyBindingStore) GetByTemp(_ context.Context, tempAuthKeyID [8]byte) (domain.TempAuthKeyBinding, bool, error) {
s.mu.RLock()
b, ok := s.m[tempAuthKeyID]
s.mu.RUnlock()
if !ok {
return domain.TempAuthKeyBinding{}, false, nil
}
b.EncryptedMessage = append([]byte(nil), b.EncryptedMessage...)
return b, true, nil
}
// ContactStore 是 store.ContactStore 的内存实现。
type ContactStore struct {
mu sync.RWMutex
m map[int64]domain.ContactList
blocks map[int64]map[int64]domain.BlockedContact
}
// NewContactStore 创建内存 ContactStore。
func NewContactStore() *ContactStore {
return &ContactStore{
m: make(map[int64]domain.ContactList),
blocks: make(map[int64]map[int64]domain.BlockedContact),
}
}
func (s *ContactStore) ListByUser(_ context.Context, userID int64) (domain.ContactList, error) {
s.mu.RLock()
list := s.m[userID]
s.mu.RUnlock()
list.Contacts = cloneContacts(list.Contacts)
list.Hash = contactListHash(list.Contacts)
return list, nil
}
func (s *ContactStore) Get(_ context.Context, userID, contactUserID int64) (domain.Contact, bool, error) {
s.mu.RLock()
list := s.m[userID]
s.mu.RUnlock()
for _, contact := range list.Contacts {
if contact.User.ID == contactUserID {
return cloneContact(contact), true, nil
}
}
return domain.Contact{}, false, nil
}
func (s *ContactStore) GetMany(_ context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.Contact, error) {
out := make(map[int64]domain.Contact, len(contactUserIDs))
if userID == 0 || len(contactUserIDs) == 0 {
return out, nil
}
want := make(map[int64]struct{}, len(contactUserIDs))
for _, id := range contactUserIDs {
if id != 0 {
want[id] = struct{}{}
}
}
s.mu.RLock()
list := s.m[userID]
s.mu.RUnlock()
for _, contact := range list.Contacts {
if _, ok := want[contact.User.ID]; ok {
out[contact.User.ID] = cloneContact(contact)
}
}
return out, nil
}
func (s *ContactStore) GetReverseContacts(_ context.Context, userID int64, ownerUserIDs []int64) (map[int64]domain.Contact, error) {
out := make(map[int64]domain.Contact, len(ownerUserIDs))
if userID == 0 || len(ownerUserIDs) == 0 {
return out, nil
}
want := make(map[int64]struct{}, len(ownerUserIDs))
for _, id := range ownerUserIDs {
if id != 0 {
want[id] = struct{}{}
}
}
s.mu.RLock()
defer s.mu.RUnlock()
for ownerID := range want {
for _, contact := range s.m[ownerID].Contacts {
if contact.User.ID == userID {
out[ownerID] = cloneContact(contact)
break
}
}
}
return out, nil
}
func (s *ContactStore) Upsert(_ context.Context, userID int64, input domain.ContactInput) (domain.Contact, error) {
contact := domain.Contact{
User: domain.User{
ID: input.ContactUserID,
Phone: input.Phone,
FirstName: input.FirstName,
LastName: input.LastName,
Contact: true,
},
FirstName: input.FirstName,
LastName: input.LastName,
Phone: input.Phone,
Note: input.Note,
NoteEntities: append([]domain.MessageEntity(nil), input.NoteEntities...),
}
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
reverse := s.m[input.ContactUserID]
for i := range reverse.Contacts {
if reverse.Contacts[i].User.ID == userID {
reverse.Contacts[i].Mutual = true
reverse.Contacts[i].User.Mutual = true
contact.Mutual = true
contact.User.Mutual = true
s.m[input.ContactUserID] = reverse
break
}
}
for i, existing := range list.Contacts {
if existing.User.ID != input.ContactUserID {
continue
}
contact.User.AccessHash = existing.User.AccessHash
contact.User.Username = existing.User.Username
contact.User.CountryCode = existing.User.CountryCode
contact.User.Verified = existing.User.Verified
contact.User.Support = existing.User.Support
if contact.Phone == "" {
contact.User.Phone = existing.User.Phone
}
if contact.FirstName == "" {
contact.User.FirstName = existing.User.FirstName
}
if contact.LastName == "" {
contact.User.LastName = existing.User.LastName
}
list.Contacts[i] = contact
list.Hash = contactListHash(list.Contacts)
s.m[userID] = list
return cloneContact(contact), nil
}
list.Contacts = append(list.Contacts, contact)
list.Hash = contactListHash(list.Contacts)
s.m[userID] = list
return cloneContact(contact), nil
}
func (s *ContactStore) UpsertMany(ctx context.Context, userID int64, inputs []domain.ContactInput) ([]domain.Contact, error) {
if len(inputs) == 0 {
return nil, nil
}
out := make([]domain.Contact, 0, len(inputs))
for _, input := range inputs {
contact, err := s.Upsert(ctx, userID, input)
if err != nil {
return nil, err
}
out = append(out, contact)
}
return out, nil
}
func (s *ContactStore) UpdateNote(_ context.Context, userID, contactUserID int64, note string, entities []domain.MessageEntity) (domain.Contact, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
for i := range list.Contacts {
if list.Contacts[i].User.ID != contactUserID {
continue
}
list.Contacts[i].Note = note
list.Contacts[i].NoteEntities = append([]domain.MessageEntity(nil), entities...)
list.Hash = contactListHash(list.Contacts)
s.m[userID] = list
return cloneContact(list.Contacts[i]), true, nil
}
return domain.Contact{}, false, nil
}
func (s *ContactStore) SetPersonalPhoto(_ context.Context, userID, contactUserID int64, photoID int64, date int) (domain.Contact, bool, error) {
_ = date
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
for i := range list.Contacts {
if list.Contacts[i].User.ID != contactUserID {
continue
}
list.Contacts[i].User.PhotoID = photoID
list.Contacts[i].User.PhotoPersonal = photoID != 0
list.Hash = contactListHash(list.Contacts)
s.m[userID] = list
return cloneContact(list.Contacts[i]), true, nil
}
return domain.Contact{}, false, nil
}
func (s *ContactStore) PersonalPhotos(_ context.Context, userID int64, contactUserIDs []int64) (map[int64]domain.ProfilePhotoRef, error) {
out := make(map[int64]domain.ProfilePhotoRef, len(contactUserIDs))
if userID == 0 || len(contactUserIDs) == 0 {
return out, nil
}
want := make(map[int64]struct{}, len(contactUserIDs))
for _, id := range contactUserIDs {
if id != 0 {
want[id] = struct{}{}
}
}
s.mu.RLock()
list := s.m[userID]
s.mu.RUnlock()
for _, contact := range list.Contacts {
if _, ok := want[contact.User.ID]; !ok || contact.User.PhotoID == 0 {
continue
}
out[contact.User.ID] = domain.ProfilePhotoRef{
PhotoID: contact.User.PhotoID,
DCID: contact.User.PhotoDCID,
Stripped: append([]byte(nil), contact.User.PhotoStripped...),
Personal: true,
}
}
return out, nil
}
func (s *ContactStore) Delete(_ context.Context, userID int64, contactUserIDs []int64) (int, error) {
remove := make(map[int64]struct{}, len(contactUserIDs))
for _, id := range contactUserIDs {
if id != 0 {
remove[id] = struct{}{}
}
}
if len(remove) == 0 {
return 0, nil
}
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
out := list.Contacts[:0]
deleted := 0
for _, contact := range list.Contacts {
if _, ok := remove[contact.User.ID]; ok {
deleted++
if reverse := s.m[contact.User.ID]; len(reverse.Contacts) > 0 {
for i := range reverse.Contacts {
if reverse.Contacts[i].User.ID == userID {
reverse.Contacts[i].Mutual = false
reverse.Contacts[i].User.Mutual = false
}
}
reverse.Hash = contactListHash(reverse.Contacts)
s.m[contact.User.ID] = reverse
}
continue
}
out = append(out, contact)
}
list.Contacts = out
list.Hash = contactListHash(list.Contacts)
s.m[userID] = list
return deleted, nil
}
func (s *ContactStore) Block(_ context.Context, userID, blockedUserID int64, date int) (bool, error) {
if userID == 0 || blockedUserID == 0 || userID == blockedUserID {
return false, nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.blocks[userID] == nil {
s.blocks[userID] = make(map[int64]domain.BlockedContact)
}
_, existed := s.blocks[userID][blockedUserID]
s.blocks[userID][blockedUserID] = domain.BlockedContact{
User: domain.User{ID: blockedUserID},
Date: date,
}
return !existed, nil
}
func (s *ContactStore) Unblock(_ context.Context, userID, blockedUserID int64) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.blocks[userID] == nil {
return false, nil
}
_, existed := s.blocks[userID][blockedUserID]
delete(s.blocks[userID], blockedUserID)
return existed, nil
}
func (s *ContactStore) IsBlocked(_ context.Context, userID, blockedUserID int64) (bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
_, blocked := s.blocks[userID][blockedUserID]
return blocked, nil
}
func (s *ContactStore) ListBlocked(_ context.Context, userID int64, offset, limit int) (domain.BlockedContactList, error) {
s.mu.RLock()
defer s.mu.RUnlock()
items := make([]domain.BlockedContact, 0, len(s.blocks[userID]))
for _, item := range s.blocks[userID] {
items = append(items, item)
}
sort.Slice(items, func(i, j int) bool {
if items[i].Date == items[j].Date {
return items[i].User.ID > items[j].User.ID
}
return items[i].Date > items[j].Date
})
total := len(items)
if offset < 0 {
offset = 0
}
if offset >= len(items) {
return domain.BlockedContactList{Count: total}, nil
}
if limit <= 0 || limit > len(items)-offset {
limit = len(items) - offset
}
out := append([]domain.BlockedContact(nil), items[offset:offset+limit]...)
return domain.BlockedContactList{Blocked: out, Count: total}, nil
}
// SaveList 保存一份用户通讯录,供测试和本地替身使用。
func (s *ContactStore) SaveList(_ context.Context, userID int64, list domain.ContactList) error {
list.Contacts = cloneContacts(list.Contacts)
list.Hash = contactListHash(list.Contacts)
s.mu.Lock()
s.m[userID] = list
s.mu.Unlock()
return nil
}
// DialogStore 是 store.DialogStore 的内存实现。
type DialogStore struct {
mu sync.RWMutex
m map[int64]domain.DialogList
drafts map[int64]map[dialogDraftKey]domain.DialogDraft
folders map[int64]map[int]domain.DialogFolder
folderOrder map[int64][]int
folderTags map[int64]bool
}
type dialogDraftKey struct {
peerType domain.PeerType
peerID int64
topMessageID int
}
// NewDialogStore 创建内存 DialogStore。
func NewDialogStore() *DialogStore {
return &DialogStore{
m: make(map[int64]domain.DialogList),
drafts: make(map[int64]map[dialogDraftKey]domain.DialogDraft),
folders: make(map[int64]map[int]domain.DialogFolder),
folderOrder: make(map[int64][]int),
folderTags: make(map[int64]bool),
}
}
func (s *DialogStore) ListByUser(_ context.Context, userID int64, filter domain.DialogFilter) (domain.DialogList, error) {
s.mu.RLock()
list := s.m[userID]
s.mu.RUnlock()
list.Dialogs = cloneDialogs(list.Dialogs)
list.Messages = cloneMessages(list.Messages)
list.Users = append([]domain.User(nil), list.Users...)
return filterDialogList(list, filter), nil
}
func (s *DialogStore) ListByPeers(_ context.Context, userID int64, peers []domain.Peer) (domain.DialogList, error) {
s.mu.RLock()
list := s.m[userID]
s.mu.RUnlock()
list.Dialogs = cloneDialogs(list.Dialogs)
list.Messages = cloneMessages(list.Messages)
list.Users = append([]domain.User(nil), list.Users...)
byPeer := make(map[domain.Peer]domain.Dialog, len(list.Dialogs))
for _, dialog := range list.Dialogs {
byPeer[dialog.Peer] = dialog
}
out := domain.DialogList{
Dialogs: make([]domain.Dialog, 0, len(peers)),
Users: make([]domain.User, 0, len(peers)),
}
seenPeers := make(map[domain.Peer]struct{}, len(peers))
seenUsers := map[int64]struct{}{}
for _, peer := range peers {
if _, ok := seenPeers[peer]; ok {
continue
}
seenPeers[peer] = struct{}{}
dialog := byPeer[peer]
if dialog.Peer.ID == 0 {
dialog.Peer = peer
}
out.Dialogs = append(out.Dialogs, dialog)
if peer.Type == domain.PeerTypeUser {
if user, ok := findDialogUser(list.Users, peer.ID); ok {
appendDialogUser(&out, seenUsers, user)
} else if peer.ID == domain.OfficialSystemUserID {
appendDialogUser(&out, seenUsers, domain.OfficialSystemUser())
}
}
}
out.Messages = keepDialogMessages(list.Messages, out.Dialogs)
out.Count = len(out.Dialogs)
out.Hash = dialogListHash(out.Dialogs)
return out, nil
}
// SaveList 保存一份用户会话列表,供测试和本地替身使用。
func (s *DialogStore) SaveList(_ context.Context, userID int64, list domain.DialogList) error {
list.Dialogs = cloneDialogs(list.Dialogs)
list.Messages = cloneMessages(list.Messages)
list.Users = append([]domain.User(nil), list.Users...)
s.mu.Lock()
s.m[userID] = list
s.mu.Unlock()
return nil
}
func (s *DialogStore) Upsert(_ context.Context, userID int64, dialog domain.Dialog) error {
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
for i, existing := range list.Dialogs {
if existing.Peer == dialog.Peer {
if dialog.FolderID == domain.DialogMainFolderID && existing.FolderID != domain.DialogMainFolderID {
dialog.FolderID = existing.FolderID
}
list.Dialogs[i] = dialog
s.m[userID] = list
return nil
}
}
list.Dialogs = append(list.Dialogs, dialog)
s.m[userID] = list
return nil
}
func (s *DialogStore) SaveDraft(_ context.Context, userID int64, draft domain.DialogDraft) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.drafts[userID] == nil {
s.drafts[userID] = make(map[dialogDraftKey]domain.DialogDraft)
}
s.drafts[userID][draftKey(draft.Peer, draft.TopMessageID)] = cloneDialogDraft(draft)
return nil
}
func (s *DialogStore) DeleteDraft(_ context.Context, userID int64, peer domain.Peer, topMessageID int) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
items := s.drafts[userID]
if len(items) == 0 {
return false, nil
}
key := draftKey(peer, topMessageID)
if _, ok := items[key]; !ok {
return false, nil
}
delete(items, key)
return true, nil
}
func (s *DialogStore) ListDrafts(_ context.Context, userID int64, limit int) ([]domain.DialogDraft, error) {
s.mu.RLock()
items := s.drafts[userID]
out := make([]domain.DialogDraft, 0, len(items))
for _, draft := range items {
out = append(out, cloneDialogDraft(draft))
}
s.mu.RUnlock()
sortDialogDrafts(out)
if limit <= 0 || limit > domain.MaxDialogDraftsPerUser {
limit = domain.MaxDialogDraftsPerUser
}
if len(out) > limit {
out = out[:limit]
}
return out, nil
}
func (s *DialogStore) ClearDrafts(_ context.Context, userID int64, limit int) ([]domain.DialogDraft, error) {
if limit <= 0 || limit > domain.MaxDialogDraftsPerUser {
limit = domain.MaxDialogDraftsPerUser
}
s.mu.Lock()
defer s.mu.Unlock()
items := s.drafts[userID]
if len(items) == 0 {
return nil, nil
}
out := make([]domain.DialogDraft, 0, len(items))
for _, draft := range items {
out = append(out, cloneDialogDraft(draft))
}
sortDialogDrafts(out)
if len(out) > limit {
out = out[:limit]
}
for _, draft := range out {
delete(items, draftKey(draft.Peer, draft.TopMessageID))
}
return out, nil
}
func (s *DialogStore) MarkRead(_ context.Context, userID int64, peer domain.Peer, maxID int) (domain.ReadHistoryResult, error) {
result := domain.ReadHistoryResult{OwnerUserID: userID, Peer: peer, MaxID: maxID}
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
for i, dialog := range list.Dialogs {
if dialog.Peer != peer {
continue
}
readMax := maxID
if readMax <= 0 {
readMax = dialog.TopMessage
}
result.MaxID = readMax
result.Changed = dialog.UnreadCount > 0 || readMax > dialog.ReadInboxMaxID
if readMax > dialog.ReadInboxMaxID {
dialog.ReadInboxMaxID = readMax
}
dialog.UnreadCount = 0
dialog.UnreadMentions = 0
dialog.UnreadReactions = 0
dialog.UnreadMark = false
result.StillUnreadCount = dialog.UnreadCount
list.Dialogs[i] = dialog
s.m[userID] = list
return result, nil
}
return result, nil
}
func (s *DialogStore) SetPinned(_ context.Context, userID int64, peer domain.Peer, pinned bool) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
nextOrder := 1
for _, dialog := range list.Dialogs {
if dialog.Pinned && dialog.PinnedOrder >= nextOrder {
nextOrder = dialog.PinnedOrder + 1
}
}
for i := range list.Dialogs {
if list.Dialogs[i].Peer != peer {
continue
}
list.Dialogs[i].Pinned = pinned
if pinned {
if list.Dialogs[i].PinnedOrder == 0 {
list.Dialogs[i].PinnedOrder = nextOrder
}
} else {
list.Dialogs[i].PinnedOrder = 0
}
s.m[userID] = list
return true, nil
}
return false, nil
}
func (s *DialogStore) ReorderPinned(_ context.Context, userID int64, order []domain.Peer, force bool) error {
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
positions := make(map[domain.Peer]int, len(order))
for i, peer := range order {
if peer.Type == "" || peer.ID == 0 {
continue
}
if _, ok := positions[peer]; ok {
continue
}
positions[peer] = len(order) - i
}
for i := range list.Dialogs {
pos, ok := positions[list.Dialogs[i].Peer]
if ok {
list.Dialogs[i].Pinned = true
list.Dialogs[i].PinnedOrder = pos
continue
}
if force && list.Dialogs[i].Pinned {
list.Dialogs[i].Pinned = false
list.Dialogs[i].PinnedOrder = 0
}
}
s.m[userID] = list
return nil
}
func (s *DialogStore) SetUnreadMark(_ context.Context, userID int64, peer domain.Peer, unread bool) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
for i := range list.Dialogs {
if list.Dialogs[i].Peer != peer {
continue
}
list.Dialogs[i].UnreadMark = unread
s.m[userID] = list
return true, nil
}
return false, nil
}
func (s *DialogStore) ListUnreadMarked(_ context.Context, userID int64) ([]domain.Peer, error) {
s.mu.RLock()
list := s.m[userID]
s.mu.RUnlock()
out := make([]domain.Peer, 0, len(list.Dialogs))
for _, dialog := range list.Dialogs {
if dialog.UnreadMark {
out = append(out, dialog.Peer)
}
}
return out, nil
}
func (s *DialogStore) SetPeerSettingsBarHidden(_ context.Context, userID int64, peer domain.Peer) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
for i := range list.Dialogs {
if list.Dialogs[i].Peer != peer {
continue
}
list.Dialogs[i].PeerSettingsBarHidden = true
s.m[userID] = list
return true, nil
}
return false, nil
}
func (s *DialogStore) PeerSettingsBarHidden(_ context.Context, userID int64, peer domain.Peer) (bool, error) {
s.mu.RLock()
list := s.m[userID]
s.mu.RUnlock()
for _, dialog := range list.Dialogs {
if dialog.Peer == peer {
return dialog.PeerSettingsBarHidden, nil
}
}
return false, nil
}
func (s *DialogStore) ListFolders(_ context.Context, userID int64) (domain.DialogFolderList, error) {
s.mu.RLock()
defer s.mu.RUnlock()
byID := s.folders[userID]
order := append([]int(nil), s.folderOrder[userID]...)
seen := make(map[int]struct{}, len(byID))
out := domain.DialogFolderList{
TagsEnabled: s.folderTags[userID],
Folders: make([]domain.DialogFolder, 0, len(byID)),
}
for _, id := range order {
folder, ok := byID[id]
if !ok {
continue
}
seen[id] = struct{}{}
out.Folders = append(out.Folders, cloneDialogFolder(folder))
}
remaining := make([]int, 0, len(byID))
for id := range byID {
if _, ok := seen[id]; !ok {
remaining = append(remaining, id)
}
}
sort.Ints(remaining)
for _, id := range remaining {
out.Folders = append(out.Folders, cloneDialogFolder(byID[id]))
}
return out, nil
}
func (s *DialogStore) GetFolder(_ context.Context, userID int64, folderID int) (domain.DialogFolder, bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
folder, ok := s.folders[userID][folderID]
if !ok {
return domain.DialogFolder{}, false, nil
}
return cloneDialogFolder(folder), true, nil
}
func (s *DialogStore) UpsertFolder(_ context.Context, userID int64, folder domain.DialogFolder) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.folders[userID] == nil {
s.folders[userID] = make(map[int]domain.DialogFolder)
}
s.folders[userID][folder.ID] = cloneDialogFolder(folder)
if !containsInt(s.folderOrder[userID], folder.ID) {
s.folderOrder[userID] = append(s.folderOrder[userID], folder.ID)
}
return nil
}
func (s *DialogStore) DeleteFolder(_ context.Context, userID int64, folderID int) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.folders[userID], folderID)
s.folderOrder[userID] = removeInt(s.folderOrder[userID], folderID)
return nil
}
func (s *DialogStore) ReorderFolders(_ context.Context, userID int64, order []int) error {
s.mu.Lock()
defer s.mu.Unlock()
byID := s.folders[userID]
seen := make(map[int]struct{}, len(order))
next := make([]int, 0, len(byID))
for _, id := range order {
if id < domain.DialogCustomFolderMinID {
continue
}
if _, ok := byID[id]; !ok {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
next = append(next, id)
}
remaining := make([]int, 0, len(byID))
for id := range byID {
if _, ok := seen[id]; !ok {
remaining = append(remaining, id)
}
}
sort.Ints(remaining)
next = append(next, remaining...)
s.folderOrder[userID] = next
return nil
}
func (s *DialogStore) SetFolderTagsEnabled(_ context.Context, userID int64, enabled bool) error {
s.mu.Lock()
s.folderTags[userID] = enabled
s.mu.Unlock()
return nil
}
func (s *DialogStore) EditPeerFolders(_ context.Context, userID int64, peers []domain.FolderPeerUpdate) error {
s.mu.Lock()
defer s.mu.Unlock()
list := s.m[userID]
updates := make(map[domain.Peer]int, len(peers))
for _, item := range peers {
if item.Peer.Type == "" || item.Peer.ID == 0 {
continue
}
updates[item.Peer] = item.FolderID
}
for i := range list.Dialogs {
if folderID, ok := updates[list.Dialogs[i].Peer]; ok {
list.Dialogs[i].FolderID = folderID
}
}
s.m[userID] = list
return nil
}
// MessageStore 是 store.MessageStore 的内存实现。
type MessageStore struct {
mu sync.RWMutex
m map[int64][]domain.Message
nextUID int64
nextBox map[int64]int
nextPts map[int64]int
readOutboxDates map[readOutboxDateKey]int
privateReactions map[int64]map[int64][]domain.ChannelMessagePeerReaction
dialogs *DialogStore
}
type readOutboxDateKey struct {
ownerUserID int64
peerID int64
msgID int
}
// NewMessageStore 创建内存 MessageStore。
func NewMessageStore(dialogs ...*DialogStore) *MessageStore {
s := &MessageStore{
m: make(map[int64][]domain.Message),
nextUID: 1,
nextBox: make(map[int64]int),
nextPts: make(map[int64]int),
readOutboxDates: make(map[readOutboxDateKey]int),
privateReactions: make(map[int64]map[int64][]domain.ChannelMessagePeerReaction),
}
if len(dialogs) > 0 {
s.dialogs = dialogs[0]
}
return s
}
func (s *MessageStore) Create(_ context.Context, msg domain.Message) (domain.Message, error) {
s.mu.Lock()
defer s.mu.Unlock()
msg.ID = s.nextBoxIDLocked(msg.OwnerUserID)
msg.UID = s.nextUID
s.nextUID++
msg.Entities = append([]domain.MessageEntity(nil), msg.Entities...)
s.m[msg.OwnerUserID] = append(s.m[msg.OwnerUserID], msg)
if s.dialogs != nil {
s.dialogs.mu.Lock()
list := s.dialogs.m[msg.OwnerUserID]
list.Messages = append(list.Messages, msg)
if msg.Peer.Type == domain.PeerTypeUser && msg.Peer.ID == domain.OfficialSystemUserID && !hasUser(list.Users, domain.OfficialSystemUserID) {
list.Users = append(list.Users, domain.OfficialSystemUser())
}
s.dialogs.m[msg.OwnerUserID] = list
s.dialogs.mu.Unlock()
}
return msg, nil
}
func (s *MessageStore) SendPrivateText(_ context.Context, req domain.SendPrivateTextRequest) (domain.SendPrivateTextResult, error) {
s.mu.Lock()
defer s.mu.Unlock()
for _, msg := range s.m[req.SenderUserID] {
if msg.RandomID != 0 && msg.RandomID == req.RandomID {
recipient := domain.Message{}
if req.SenderUserID != req.RecipientUserID {
for _, peerMsg := range s.m[req.RecipientUserID] {
if peerMsg.UID == msg.UID {
recipient = peerMsg
break
}
}
} else {
recipient = msg
}
return domain.SendPrivateTextResult{
SenderMessage: cloneMessage(msg),
RecipientMessage: cloneMessage(recipient),
SenderEvent: newMessageEvent(msg),
RecipientEvent: newMessageEvent(recipient),
Duplicate: true,
}, nil
}
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
senderReply, recipientReply, err := s.resolveMemoryReplyLocked(req)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
uid := s.nextUID
s.nextUID++
sender := domain.Message{
ID: s.nextBoxIDLocked(req.SenderUserID),
UID: uid,
RandomID: req.RandomID,
OwnerUserID: req.SenderUserID,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: req.RecipientUserID},
From: domain.Peer{Type: domain.PeerTypeUser, ID: req.SenderUserID},
Date: req.Date,
Out: true,
Silent: req.Silent,
NoForwards: req.NoForwards,
Body: req.Message,
Entities: append([]domain.MessageEntity(nil), req.Entities...),
Media: req.Media,
ReplyTo: cloneMessageReply(senderReply),
Forward: cloneMessageForward(req.Forward),
Pts: s.nextPtsLocked(req.SenderUserID),
}
recipient := domain.Message{}
if req.SenderUserID == req.RecipientUserID {
recipient = sender
}
if req.SenderUserID != req.RecipientUserID && !req.RecipientBlocked {
recipient = sender
recipient.ID = s.nextBoxIDLocked(req.RecipientUserID)
recipient.OwnerUserID = req.RecipientUserID
recipient.Peer = domain.Peer{Type: domain.PeerTypeUser, ID: req.SenderUserID}
recipient.Out = false
recipient.ReplyTo = cloneMessageReply(recipientReply)
recipient.Pts = s.nextPtsLocked(req.RecipientUserID)
recipient.MediaUnread = !req.Media.IsZero()
}
s.m[req.SenderUserID] = append(s.m[req.SenderUserID], sender)
if req.SenderUserID != req.RecipientUserID && !req.RecipientBlocked {
s.m[req.RecipientUserID] = append(s.m[req.RecipientUserID], recipient)
}
if s.dialogs != nil {
if recipient.ID != 0 {
s.upsertMemoryDialogsLocked(sender, recipient)
} else {
s.upsertMemoryDialogsLocked(sender, sender)
}
}
return domain.SendPrivateTextResult{
SenderMessage: cloneMessage(sender),
RecipientMessage: cloneMessage(recipient),
SenderEvent: newMessageEvent(sender),
RecipientEvent: newMessageEvent(recipient),
}, nil
}
func (s *MessageStore) resolveMemoryReplyLocked(req domain.SendPrivateTextRequest) (*domain.MessageReply, *domain.MessageReply, error) {
if req.ReplyTo == nil {
return nil, nil, nil
}
if err := domain.ValidateMessageReplyBounds(req.ReplyTo); err != nil {
return nil, nil, err
}
peer := req.ReplyTo.Peer
if peer.ID == 0 {
peer = domain.Peer{Type: domain.PeerTypeUser, ID: req.RecipientUserID}
}
if peer.Type != domain.PeerTypeUser || peer.ID != req.RecipientUserID {
return nil, nil, domain.ErrReplyMessageIDInvalid
}
var target domain.Message
for _, msg := range s.m[req.SenderUserID] {
if msg.Peer == peer && msg.ID == req.ReplyTo.MessageID {
target = msg
break
}
}
if target.ID == 0 {
return nil, nil, domain.ErrReplyMessageIDInvalid
}
senderReply := cloneMessageReply(req.ReplyTo)
senderReply.MessageID = target.ID
senderReply.Peer = peer
if req.SenderUserID == req.RecipientUserID {
return senderReply, cloneMessageReply(senderReply), nil
}
for _, msg := range s.m[req.RecipientUserID] {
if msg.UID == target.UID {
recipientReply := cloneMessageReply(senderReply)
recipientReply.MessageID = msg.ID
recipientReply.Peer = domain.Peer{Type: domain.PeerTypeUser, ID: req.SenderUserID}
return senderReply, recipientReply, nil
}
}
return senderReply, nil, nil
}
func (s *MessageStore) ForwardPrivateMessages(ctx context.Context, req domain.ForwardPrivateMessagesRequest) (domain.ForwardPrivateMessagesResult, error) {
res := domain.ForwardPrivateMessagesResult{OwnerUserID: req.OwnerUserID}
if req.OwnerUserID == 0 || req.ToUserID == 0 || req.FromPeer.Type != domain.PeerTypeUser || req.FromPeer.ID == 0 {
return res, domain.ErrMessageIDInvalid
}
if len(req.MessageIDs) == 0 || len(req.MessageIDs) != len(req.RandomIDs) {
return res, domain.ErrMessageIDInvalid
}
if len(req.MessageIDs) > domain.MaxForwardMessageIDs {
return res, domain.ErrMessageIDInvalid
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
s.mu.RLock()
sources := make([]domain.Message, 0, len(req.MessageIDs))
for _, id := range req.MessageIDs {
if id <= 0 || id > domain.MaxMessageBoxID {
s.mu.RUnlock()
return res, domain.ErrMessageIDInvalid
}
var source domain.Message
for _, msg := range s.m[req.OwnerUserID] {
if msg.Peer == req.FromPeer && msg.ID == id {
source = cloneMessage(msg)
break
}
}
if source.ID == 0 {
s.mu.RUnlock()
return res, domain.ErrMessageIDInvalid
}
if source.NoForwards {
s.mu.RUnlock()
return res, domain.ErrChatForwardsRestricted
}
sources = append(sources, source)
}
s.mu.RUnlock()
res.SenderMessages = make([]domain.Message, 0, len(sources))
res.RecipientMessages = make([]domain.Message, 0, len(sources))
res.SenderEvents = make([]domain.UpdateEvent, 0, len(sources))
res.RecipientEvents = make([]domain.UpdateEvent, 0, len(sources))
res.Duplicates = make([]bool, 0, len(sources))
for i, source := range sources {
if req.RandomIDs[i] == 0 {
return res, domain.ErrMessageIDInvalid
}
var forward *domain.MessageForward
if !req.DropAuthor {
forward = cloneMessageForward(source.Forward)
if forward == nil {
forward = &domain.MessageForward{From: source.From, Date: source.Date}
}
}
sent, err := s.SendPrivateText(ctx, domain.SendPrivateTextRequest{
SenderUserID: req.OwnerUserID,
RecipientUserID: req.ToUserID,
RandomID: req.RandomIDs[i],
Message: source.Body,
Entities: append([]domain.MessageEntity(nil), source.Entities...),
Silent: req.Silent,
NoForwards: req.NoForwards,
ReplyTo: req.ReplyTo,
Forward: forward,
Date: req.Date,
OriginAuthKeyID: req.OriginAuthKeyID,
OriginSessionID: req.OriginSessionID,
RecipientBlocked: req.RecipientBlocked,
})
if err != nil {
return res, err
}
res.SenderMessages = append(res.SenderMessages, sent.SenderMessage)
res.RecipientMessages = append(res.RecipientMessages, sent.RecipientMessage)
res.SenderEvents = append(res.SenderEvents, sent.SenderEvent)
res.RecipientEvents = append(res.RecipientEvents, sent.RecipientEvent)
res.Duplicates = append(res.Duplicates, sent.Duplicate)
}
return res, nil
}
func (s *MessageStore) GetByIDs(_ context.Context, userID int64, ids []int) (domain.MessageList, error) {
if userID == 0 || len(ids) == 0 {
return domain.MessageList{}, nil
}
s.mu.RLock()
byID := make(map[int]domain.Message, len(s.m[userID]))
for _, msg := range s.m[userID] {
item := cloneMessage(msg)
reactions := s.privateMessageReactionsLocked(item.UID, item.OwnerUserID)
if len(reactions.Results) > 0 || len(reactions.Recent) > 0 {
item.Reactions = cloneChannelMessageReactionsPtr(&reactions)
}
byID[msg.ID] = item
}
s.mu.RUnlock()
out := domain.MessageList{Messages: make([]domain.Message, 0, len(ids))}
for _, id := range ids {
if msg, ok := byID[id]; ok {
out.Messages = append(out.Messages, msg)
}
}
out.Users = usersForMessages(out.Messages)
out.Hash = messageListHash(out.Messages)
return out, nil
}
func (s *MessageStore) ListByUser(_ context.Context, userID int64, filter domain.MessageFilter) (domain.MessageList, error) {
s.mu.RLock()
messages := cloneMessages(s.m[userID])
for i := range messages {
reactions := s.privateMessageReactionsLocked(messages[i].UID, messages[i].OwnerUserID)
if len(reactions.Results) > 0 || len(reactions.Recent) > 0 {
messages[i].Reactions = cloneChannelMessageReactionsPtr(&reactions)
}
}
s.mu.RUnlock()
return filterMessageList(messages, filter), nil
}
func (s *MessageStore) ReadHistory(_ context.Context, req domain.ReadHistoryRequest) (domain.ReadHistoryResult, error) {
res := domain.ReadHistoryResult{OwnerUserID: req.OwnerUserID, Peer: req.Peer, MaxID: req.MaxID}
if req.OwnerUserID == 0 || req.Peer.ID == 0 {
return res, nil
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
if s.dialogs == nil {
return res, nil
}
s.dialogs.mu.Lock()
defer s.dialogs.mu.Unlock()
list := s.dialogs.m[req.OwnerUserID]
for i, dialog := range list.Dialogs {
if dialog.Peer != req.Peer {
continue
}
readMax := req.MaxID
if readMax <= 0 {
readMax = dialog.TopMessage
}
if readMax > domain.MaxMessageBoxID {
readMax = domain.MaxMessageBoxID
}
oldRead := dialog.ReadInboxMaxID
res.MaxID = readMax
res.Changed = dialog.UnreadCount > 0 || readMax > oldRead
if !res.Changed {
return res, nil
}
var latestIncoming domain.Message
unread := 0
for _, msg := range s.m[req.OwnerUserID] {
if msg.Peer != req.Peer || msg.Out {
continue
}
if msg.ID > readMax {
unread++
continue
}
if msg.ID > oldRead && msg.ID > latestIncoming.ID {
latestIncoming = msg
}
}
if readMax > dialog.ReadInboxMaxID {
dialog.ReadInboxMaxID = readMax
}
dialog.UnreadCount = unread
dialog.UnreadMentions = 0
dialog.UnreadReactions = 0
dialog.UnreadMark = false
res.StillUnreadCount = unread
pts := s.nextPtsLocked(req.OwnerUserID)
res.InboxEvent = domain.UpdateEvent{
UserID: req.OwnerUserID,
Type: domain.UpdateEventReadHistoryInbox,
Pts: pts,
PtsCount: 1,
Date: req.Date,
Peer: req.Peer,
MaxID: readMax,
StillUnreadCount: unread,
}
list.Dialogs[i] = dialog
s.dialogs.m[req.OwnerUserID] = list
if latestIncoming.ID != 0 && latestIncoming.From.ID != 0 && latestIncoming.From.ID != req.OwnerUserID {
senderUserID := latestIncoming.From.ID
senderBoxID := 0
for _, msg := range s.m[senderUserID] {
if msg.UID == latestIncoming.UID && msg.Out {
senderBoxID = msg.ID
break
}
}
if senderBoxID > 0 {
senderList := s.dialogs.m[senderUserID]
for j, senderDialog := range senderList.Dialogs {
if senderDialog.Peer != (domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID}) {
continue
}
if senderBoxID <= senderDialog.ReadOutboxMaxID {
break
}
oldOutbox := senderDialog.ReadOutboxMaxID
senderDialog.ReadOutboxMaxID = senderBoxID
senderList.Dialogs[j] = senderDialog
s.dialogs.m[senderUserID] = senderList
for _, msg := range s.m[senderUserID] {
if msg.Peer == (domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID}) && msg.Out && msg.ID > oldOutbox && msg.ID <= senderBoxID {
s.readOutboxDates[readOutboxDateKey{ownerUserID: senderUserID, peerID: req.OwnerUserID, msgID: msg.ID}] = req.Date
}
}
outPts := s.nextPtsLocked(senderUserID)
res.OutboxChanged = true
res.OutboxUserID = senderUserID
res.OutboxEvent = domain.UpdateEvent{
UserID: senderUserID,
Type: domain.UpdateEventReadHistoryOutbox,
Pts: outPts,
PtsCount: 1,
Date: req.Date,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID},
MaxID: senderBoxID,
}
break
}
}
}
return res, nil
}
return res, nil
}
func (s *MessageStore) ReadMessageContents(_ context.Context, req domain.ReadMessageContentsRequest) (domain.ReadMessageContentsResult, error) {
res := domain.ReadMessageContentsResult{OwnerUserID: req.OwnerUserID}
if req.OwnerUserID == 0 {
return res, fmt.Errorf("read message contents: missing owner user id")
}
if len(req.IDs) > domain.MaxGetMessageIDs {
return res, domain.ErrMessageIDInvalid
}
wanted := make(map[int]struct{}, len(req.IDs))
for _, id := range req.IDs {
if id <= 0 || id > domain.MaxMessageBoxID {
return res, domain.ErrMessageIDInvalid
}
wanted[id] = struct{}{}
}
if len(wanted) == 0 {
return res, nil
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
affectedPeers := make(map[domain.Peer]struct{})
for i := range s.m[req.OwnerUserID] {
msg := &s.m[req.OwnerUserID][i]
if _, ok := wanted[msg.ID]; !ok {
continue
}
if !msg.MediaUnread && !msg.ReactionUnread {
continue
}
if msg.ReactionUnread && msg.Peer.ID != 0 {
affectedPeers[msg.Peer] = struct{}{}
}
msg.MediaUnread = false
msg.ReactionUnread = false
res.MessageIDs = append(res.MessageIDs, msg.ID)
}
sort.Ints(res.MessageIDs)
if len(res.MessageIDs) == 0 {
return res, nil
}
if s.dialogs != nil && len(affectedPeers) > 0 {
s.dialogs.mu.Lock()
list := s.dialogs.m[req.OwnerUserID]
for i := range list.Dialogs {
if _, ok := affectedPeers[list.Dialogs[i].Peer]; !ok {
continue
}
list.Dialogs[i].UnreadReactions = s.countPrivateUnreadReactionsLocked(req.OwnerUserID, list.Dialogs[i].Peer)
}
s.dialogs.m[req.OwnerUserID] = list
s.dialogs.mu.Unlock()
}
pts := s.nextPtsNLocked(req.OwnerUserID, len(res.MessageIDs))
res.Event = domain.UpdateEvent{
UserID: req.OwnerUserID,
Type: domain.UpdateEventReadMessageContents,
Pts: pts,
PtsCount: len(res.MessageIDs),
Date: req.Date,
MessageIDs: append([]int(nil), res.MessageIDs...),
}
return res, nil
}
func (s *MessageStore) GetOutboxReadDate(_ context.Context, req domain.OutboxReadDateRequest) (int, error) {
if req.OwnerUserID == 0 || req.Peer.Type != domain.PeerTypeUser || req.Peer.ID == 0 || req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return 0, domain.ErrMessageIDInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
found := false
for _, msg := range s.m[req.OwnerUserID] {
if msg.ID == req.ID && msg.Peer == req.Peer && msg.Out {
found = true
break
}
}
if !found {
return 0, domain.ErrMessageIDInvalid
}
date := s.readOutboxDates[readOutboxDateKey{ownerUserID: req.OwnerUserID, peerID: req.Peer.ID, msgID: req.ID}]
if date == 0 {
return 0, domain.ErrMessageNotReadYet
}
return date, nil
}
func (s *MessageStore) SetMessageReactions(_ context.Context, req domain.SetPrivateMessageReactionsRequest) (domain.PrivateMessageReactionsResult, error) {
if req.UserID == 0 || req.Peer.Type != domain.PeerTypeUser || req.Peer.ID == 0 || req.MessageID <= 0 || req.MessageID > domain.MaxMessageBoxID {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
if len(req.Reactions) > domain.MaxChannelMessageReactionsPerUser {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
var target domain.Message
for _, msg := range s.m[req.UserID] {
if msg.ID == req.MessageID && msg.Peer == req.Peer {
target = msg
break
}
}
if target.ID == 0 || target.UID == 0 {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
if _, ok := s.privateReactions[target.UID]; !ok {
s.privateReactions[target.UID] = make(map[int64][]domain.ChannelMessagePeerReaction)
}
rows := make([]domain.ChannelMessagePeerReaction, 0, len(req.Reactions))
for i, reaction := range req.Reactions {
if reaction.Type != domain.MessageReactionEmoji || strings.TrimSpace(reaction.Emoticon) == "" {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
rows = append(rows, domain.ChannelMessagePeerReaction{
UserID: req.UserID,
Reaction: reaction,
Big: req.Big,
My: true,
ChosenOrder: i + 1,
Date: req.Date,
})
}
if len(rows) == 0 {
delete(s.privateReactions[target.UID], req.UserID)
} else {
s.privateReactions[target.UID][req.UserID] = rows
}
if target.From.ID != 0 && target.From.ID != req.UserID {
for i := range s.m[target.From.ID] {
if s.m[target.From.ID][i].UID != target.UID {
continue
}
s.m[target.From.ID][i].ReactionUnread = len(rows) > 0
if s.dialogs != nil {
s.dialogs.mu.Lock()
list := s.dialogs.m[target.From.ID]
peer := domain.Peer{Type: domain.PeerTypeUser, ID: req.UserID}
for j := range list.Dialogs {
if list.Dialogs[j].Peer == peer {
list.Dialogs[j].UnreadReactions = s.countPrivateUnreadReactionsLocked(target.From.ID, peer)
}
}
s.dialogs.m[target.From.ID] = list
s.dialogs.mu.Unlock()
}
break
}
}
return s.privateReactionResultLocked(target.UID), nil
}
func (s *MessageStore) GetMessageReactions(_ context.Context, req domain.PrivateMessageReactionsRequest) (domain.PrivateMessageReactionsResult, error) {
if req.OwnerUserID == 0 || req.Peer.Type != domain.PeerTypeUser || req.Peer.ID == 0 || len(req.IDs) > domain.MaxGetMessageIDs {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
ids := make(map[int]struct{}, len(req.IDs))
for _, id := range req.IDs {
if id <= 0 || id > domain.MaxMessageBoxID {
return domain.PrivateMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
ids[id] = struct{}{}
}
s.mu.RLock()
defer s.mu.RUnlock()
out := domain.PrivateMessageReactionsResult{}
for _, msg := range s.m[req.OwnerUserID] {
if msg.Peer != req.Peer {
continue
}
if _, ok := ids[msg.ID]; !ok {
continue
}
item := cloneMessage(msg)
reactions := s.privateMessageReactionsLocked(msg.UID, msg.OwnerUserID)
item.Reactions = cloneChannelMessageReactionsPtr(&reactions)
out.Messages = append(out.Messages, item)
if len(out.Reactions.Results) == 0 && len(out.Reactions.Recent) == 0 {
out.Reactions = reactions
}
}
return out, nil
}
func (s *MessageStore) privateReactionResultLocked(uid int64) domain.PrivateMessageReactionsResult {
out := domain.PrivateMessageReactionsResult{}
for _, messages := range s.m {
for _, msg := range messages {
if msg.UID != uid {
continue
}
item := cloneMessage(msg)
reactions := s.privateMessageReactionsLocked(uid, msg.OwnerUserID)
item.Reactions = cloneChannelMessageReactionsPtr(&reactions)
out.Messages = append(out.Messages, item)
if len(out.Reactions.Results) == 0 && len(out.Reactions.Recent) == 0 {
out.Reactions = reactions
}
}
}
return out
}
func (s *MessageStore) privateMessageReactionsLocked(uid, viewerUserID int64) domain.ChannelMessageReactions {
byUser := s.privateReactions[uid]
out := domain.ChannelMessageReactions{CanSeeList: true}
if len(byUser) == 0 {
return out
}
counts := make(map[string]int)
recent := make([]domain.ChannelMessagePeerReaction, 0, len(byUser))
for userID, rows := range byUser {
for _, row := range rows {
key := string(row.Reaction.Type) + "\x00" + row.Reaction.Emoticon
index, ok := counts[key]
if !ok {
out.Results = append(out.Results, domain.ChannelMessageReactionCount{Reaction: row.Reaction})
index = len(out.Results) - 1
counts[key] = index
}
out.Results[index].Count++
if userID == viewerUserID && (out.Results[index].ChosenOrder == 0 || row.ChosenOrder < out.Results[index].ChosenOrder) {
out.Results[index].ChosenOrder = row.ChosenOrder
}
item := row
item.UserID = userID
item.My = userID == viewerUserID
recent = append(recent, item)
}
}
sort.Slice(out.Results, func(i, j int) bool {
if out.Results[i].Count != out.Results[j].Count {
return out.Results[i].Count > out.Results[j].Count
}
return out.Results[i].Reaction.Emoticon < out.Results[j].Reaction.Emoticon
})
sort.Slice(recent, func(i, j int) bool {
if recent[i].Date != recent[j].Date {
return recent[i].Date > recent[j].Date
}
return recent[i].UserID < recent[j].UserID
})
if len(recent) > domain.MaxChannelMessageReactionRecent {
recent = recent[:domain.MaxChannelMessageReactionRecent]
}
out.Recent = recent
return out
}
func (s *MessageStore) EditMessage(_ context.Context, req domain.EditMessageRequest) (domain.EditMessageResult, error) {
res := domain.EditMessageResult{OwnerUserID: req.OwnerUserID}
if req.OwnerUserID == 0 || req.Peer.ID == 0 || req.ID <= 0 || req.ID > domain.MaxMessageBoxID {
return res, domain.ErrMessageIDInvalid
}
if req.EditDate == 0 {
req.EditDate = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
targetIndex := -1
var target domain.Message
for i, msg := range s.m[req.OwnerUserID] {
if msg.ID == req.ID && msg.Peer == req.Peer {
targetIndex = i
target = msg
break
}
}
if targetIndex < 0 {
return res, domain.ErrMessageIDInvalid
}
if !target.Out || target.From.ID != req.OwnerUserID {
return res, domain.ErrMessageAuthorRequired
}
if target.Body == req.Message && equalMessageEntities(target.Entities, req.Entities) {
return res, domain.ErrMessageNotModified
}
for userID, messages := range s.m {
for i, msg := range messages {
if msg.UID == target.UID && msg.From.ID == req.OwnerUserID {
msg.Body = req.Message
msg.Entities = append([]domain.MessageEntity(nil), req.Entities...)
msg.EditDate = req.EditDate
msg.Pts = s.nextPtsLocked(userID)
s.m[userID][i] = msg
event := editMessageEvent(msg)
res.Edited = append(res.Edited, domain.EditedMessageForUser{
UserID: userID,
Message: cloneMessage(msg),
Event: event,
})
}
}
}
if s.dialogs != nil {
s.dialogs.mu.Lock()
for userID := range s.dialogs.m {
list := s.dialogs.m[userID]
list.Messages = cloneMessages(s.m[userID])
s.dialogs.m[userID] = list
}
s.dialogs.mu.Unlock()
}
sort.Slice(res.Edited, func(i, j int) bool { return res.Edited[i].UserID < res.Edited[j].UserID })
return res, nil
}
func (s *MessageStore) DeleteMessages(_ context.Context, req domain.DeleteMessagesRequest) (domain.DeleteMessagesResult, error) {
res := domain.DeleteMessagesResult{OwnerUserID: req.OwnerUserID}
ids := normalizeMemoryMessageIDs(req.IDs)
if req.OwnerUserID == 0 || len(ids) == 0 {
return res, nil
}
if len(ids) > domain.MaxDeleteMessageIDs {
return res, fmt.Errorf("delete messages: too many ids: %d > %d", len(ids), domain.MaxDeleteMessageIDs)
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
idSet := make(map[int]struct{}, len(ids))
for _, id := range ids {
idSet[id] = struct{}{}
}
s.mu.Lock()
defer s.mu.Unlock()
deleted, revokeUIDs, _ := s.deleteMemoryMessagesLocked(req.OwnerUserID, 0, func(msg domain.Message) bool {
_, ok := idSet[msg.ID]
return ok
})
if req.Revoke && len(revokeUIDs) > 0 {
deleted = append(deleted, s.deleteMemoryMessagesByUIDLocked(revokeUIDs, req.OwnerUserID)...)
}
return s.finishMemoryDeleteLocked(res, deleted, req.Date, false), nil
}
func (s *MessageStore) DeleteHistory(_ context.Context, req domain.DeleteHistoryRequest) (domain.DeleteMessagesResult, error) {
res := domain.DeleteMessagesResult{OwnerUserID: req.OwnerUserID}
if req.OwnerUserID == 0 || req.Peer.ID == 0 {
return res, nil
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
deleted, revokeUIDs, more := s.deleteMemoryMessagesLocked(req.OwnerUserID, domain.MaxDeleteHistoryBatch, func(msg domain.Message) bool {
return msg.Peer == req.Peer && (req.MaxID <= 0 || msg.ID <= req.MaxID)
})
if req.Revoke && len(revokeUIDs) > 0 {
deleted = append(deleted, s.deleteMemoryMessagesByUIDLocked(revokeUIDs, req.OwnerUserID)...)
}
res = s.finishMemoryDeleteLocked(res, deleted, req.Date, req.JustClear)
if more {
res.Offset = 1
}
return res, nil
}
func (s *MessageStore) nextBoxIDLocked(userID int64) int {
next := s.nextBox[userID] + 1
s.nextBox[userID] = next
return next
}
func (s *MessageStore) nextPtsLocked(userID int64) int {
next := s.nextPts[userID] + 1
s.nextPts[userID] = next
return next
}
func (s *MessageStore) nextPtsNLocked(userID int64, count int) int {
if count <= 0 {
count = 1
}
next := s.nextPts[userID] + count
s.nextPts[userID] = next
return next
}
func (s *MessageStore) countPrivateUnreadReactionsLocked(ownerUserID int64, peer domain.Peer) int {
count := 0
for _, msg := range s.m[ownerUserID] {
if msg.Peer == peer && msg.ReactionUnread {
count++
}
}
return count
}
func (s *MessageStore) upsertMemoryDialogsLocked(sender, recipient domain.Message) {
s.dialogs.mu.Lock()
defer s.dialogs.mu.Unlock()
list := s.dialogs.m[sender.OwnerUserID]
list = upsertMemoryDialog(list, domain.Dialog{Peer: sender.Peer, TopMessage: sender.ID, TopMessageDate: sender.Date})
list.Messages = append(list.Messages, sender)
s.dialogs.m[sender.OwnerUserID] = list
if recipient.OwnerUserID != sender.OwnerUserID {
peerList := s.dialogs.m[recipient.OwnerUserID]
peerList = upsertMemoryDialog(peerList, domain.Dialog{Peer: recipient.Peer, TopMessage: recipient.ID, TopMessageDate: recipient.Date, UnreadCount: 1})
peerList.Messages = append(peerList.Messages, recipient)
s.dialogs.m[recipient.OwnerUserID] = peerList
}
}
func upsertMemoryDialog(list domain.DialogList, dialog domain.Dialog) domain.DialogList {
for i := range list.Dialogs {
if list.Dialogs[i].Peer == dialog.Peer {
if dialog.ReadInboxMaxID == 0 {
dialog.ReadInboxMaxID = list.Dialogs[i].ReadInboxMaxID
}
if dialog.ReadOutboxMaxID == 0 {
dialog.ReadOutboxMaxID = list.Dialogs[i].ReadOutboxMaxID
}
if dialog.FolderID == domain.DialogMainFolderID && list.Dialogs[i].FolderID != domain.DialogMainFolderID {
dialog.FolderID = list.Dialogs[i].FolderID
}
if dialog.UnreadCount != 0 {
dialog.UnreadCount += list.Dialogs[i].UnreadCount
}
list.Dialogs[i] = dialog
return list
}
}
list.Dialogs = append(list.Dialogs, dialog)
return list
}
type deletedMemoryMessage struct {
userID int64
peer domain.Peer
id int
}
func (s *MessageStore) deleteMemoryMessagesLocked(userID int64, limit int, match func(domain.Message) bool) ([]deletedMemoryMessage, map[int64]struct{}, bool) {
messages := s.m[userID]
kept := messages[:0]
deleted := make([]deletedMemoryMessage, 0)
revokeUIDs := make(map[int64]struct{})
more := false
for _, msg := range messages {
if match(msg) {
if limit > 0 && len(deleted) >= limit {
kept = append(kept, msg)
more = true
continue
}
deleted = append(deleted, deletedMemoryMessage{userID: userID, peer: msg.Peer, id: msg.ID})
if msg.UID != 0 {
revokeUIDs[msg.UID] = struct{}{}
}
continue
}
kept = append(kept, msg)
}
s.m[userID] = kept
return deleted, revokeUIDs, more
}
func (s *MessageStore) deleteMemoryMessagesByUIDLocked(uids map[int64]struct{}, excludeUserID int64) []deletedMemoryMessage {
if len(uids) == 0 {
return nil
}
deleted := make([]deletedMemoryMessage, 0)
for userID, messages := range s.m {
if userID == excludeUserID {
continue
}
kept := messages[:0]
for _, msg := range messages {
if _, ok := uids[msg.UID]; ok {
deleted = append(deleted, deletedMemoryMessage{userID: userID, peer: msg.Peer, id: msg.ID})
continue
}
kept = append(kept, msg)
}
s.m[userID] = kept
}
return deleted
}
func (s *MessageStore) finishMemoryDeleteLocked(res domain.DeleteMessagesResult, deleted []deletedMemoryMessage, date int, preserveEmptyDialogs bool) domain.DeleteMessagesResult {
if len(deleted) == 0 {
return res
}
idsByOwner := make(map[int64][]int)
peersByOwner := make(map[int64]map[domain.Peer]struct{})
for _, row := range deleted {
idsByOwner[row.userID] = append(idsByOwner[row.userID], row.id)
if peersByOwner[row.userID] == nil {
peersByOwner[row.userID] = make(map[domain.Peer]struct{})
}
peersByOwner[row.userID][row.peer] = struct{}{}
}
if s.dialogs != nil {
s.dialogs.mu.Lock()
for userID, peers := range peersByOwner {
for peer := range peers {
s.rebuildMemoryDialogLocked(userID, peer, preserveEmptyDialogs)
}
}
s.dialogs.mu.Unlock()
}
ownerIDs := make([]int64, 0, len(idsByOwner))
for userID := range idsByOwner {
ownerIDs = append(ownerIDs, userID)
}
sort.Slice(ownerIDs, func(i, j int) bool { return ownerIDs[i] < ownerIDs[j] })
for _, userID := range ownerIDs {
ids := normalizeMemoryMessageIDs(idsByOwner[userID])
if len(ids) == 0 {
continue
}
pts := s.nextPtsNLocked(userID, len(ids))
event := domain.UpdateEvent{
UserID: userID,
Type: domain.UpdateEventDeleteMessages,
Pts: pts,
PtsCount: len(ids),
Date: date,
MessageIDs: ids,
}
res.Deleted = append(res.Deleted, domain.DeletedMessagesForUser{
UserID: userID,
MessageIDs: ids,
Event: event,
})
}
return res
}
func (s *MessageStore) rebuildMemoryDialogLocked(userID int64, peer domain.Peer, preserveEmpty bool) {
list := s.dialogs.m[userID]
topID := 0
topDate := 0
unread := 0
for _, msg := range s.m[userID] {
if msg.Peer != peer {
continue
}
if msg.ID > topID {
topID = msg.ID
topDate = msg.Date
}
}
dialogs := list.Dialogs[:0]
for _, dialog := range list.Dialogs {
if dialog.Peer != peer {
dialogs = append(dialogs, dialog)
continue
}
if topID == 0 {
if preserveEmpty {
oldTop := dialog.TopMessage
dialog.TopMessage = 0
dialog.TopMessageDate = 0
if dialog.ReadInboxMaxID < oldTop {
dialog.ReadInboxMaxID = oldTop
}
if dialog.ReadOutboxMaxID < oldTop {
dialog.ReadOutboxMaxID = oldTop
}
dialog.UnreadCount = 0
dialog.UnreadMark = false
dialog.UnreadMentions = 0
dialog.UnreadReactions = 0
dialogs = append(dialogs, dialog)
}
continue
}
for _, msg := range s.m[userID] {
if msg.Peer == peer && !msg.Out && msg.ID > dialog.ReadInboxMaxID {
unread++
}
}
dialog.TopMessage = topID
dialog.TopMessageDate = topDate
dialog.UnreadCount = unread
dialog.UnreadMentions = 0
dialog.UnreadReactions = 0
dialogs = append(dialogs, dialog)
}
list.Dialogs = dialogs
list.Messages = cloneMessages(s.m[userID])
s.dialogs.m[userID] = list
}
func normalizeMemoryMessageIDs(ids []int) []int {
if len(ids) == 0 {
return nil
}
out := make([]int, 0, len(ids))
seen := make(map[int]struct{}, len(ids))
for _, id := range ids {
if id <= 0 || id > domain.MaxMessageBoxID {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
sort.Ints(out)
return out
}
func cloneMessage(msg domain.Message) domain.Message {
msg.Entities = append([]domain.MessageEntity(nil), msg.Entities...)
msg.ReplyTo = cloneMessageReply(msg.ReplyTo)
msg.Forward = cloneMessageForward(msg.Forward)
msg.Reactions = cloneChannelMessageReactionsPtr(msg.Reactions)
return msg
}
func cloneMessageReply(reply *domain.MessageReply) *domain.MessageReply {
if reply == nil {
return nil
}
clone := *reply
clone.QuoteEntities = append([]domain.MessageEntity(nil), reply.QuoteEntities...)
return &clone
}
func cloneMessageForward(forward *domain.MessageForward) *domain.MessageForward {
if forward == nil {
return nil
}
clone := *forward
return &clone
}
func newMessageEvent(msg domain.Message) domain.UpdateEvent {
if msg.ID == 0 {
return domain.UpdateEvent{}
}
return domain.UpdateEvent{
UserID: msg.OwnerUserID,
Type: domain.UpdateEventNewMessage,
Pts: msg.Pts,
PtsCount: 1,
Date: msg.Date,
Message: cloneMessage(msg),
}
}
func editMessageEvent(msg domain.Message) domain.UpdateEvent {
if msg.ID == 0 {
return domain.UpdateEvent{}
}
return domain.UpdateEvent{
UserID: msg.OwnerUserID,
Type: domain.UpdateEventEditMessage,
Pts: msg.Pts,
PtsCount: 1,
Date: msg.EditDate,
Message: cloneMessage(msg),
}
}
func equalMessageEntities(a, b []domain.MessageEntity) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func hasUser(users []domain.User, id int64) bool {
for _, u := range users {
if u.ID == id {
return true
}
}
return false
}
func filterMessageList(messages []domain.Message, filter domain.MessageFilter) domain.MessageList {
filter.AddOffset = domain.ClampMessageHistoryAddOffset(filter.AddOffset)
sort.SliceStable(messages, func(i, j int) bool {
return messageLess(messages[i], messages[j])
})
query := strings.ToLower(filter.Query)
base := make([]domain.Message, 0, len(messages))
for _, msg := range messages {
if filter.HasPeer && msg.Peer != filter.Peer {
continue
}
if query != "" && !strings.Contains(strings.ToLower(msg.Body), query) {
continue
}
if filter.MaxID > 0 && msg.ID >= filter.MaxID {
continue
}
if filter.MinID > 0 && msg.ID <= filter.MinID {
continue
}
base = append(base, msg)
}
limit := filter.Limit
if limit <= 0 {
limit = 100
}
if limit > 500 {
limit = 500
}
page := pageMessageHistory(base, filter, limit)
return domain.MessageList{
Messages: page,
Users: usersForMessages(page),
Count: len(base),
Hash: messageListHash(base),
}
}
func pageMessageHistory(base []domain.Message, filter domain.MessageFilter, limit int) []domain.Message {
if limit <= 0 || len(base) == 0 {
return nil
}
switch messageHistoryLoadType(filter.AddOffset, limit) {
case messageHistoryLoadForward:
return cloneMessages(forwardMessageHistory(base, filter, limit))
case messageHistoryLoadAround:
forwardLimit := -filter.AddOffset
if forwardLimit > limit {
forwardLimit = limit
}
backwardLimit := limit + filter.AddOffset
if backwardLimit < 0 {
backwardLimit = 0
}
page := make([]domain.Message, 0, limit)
page = append(page, forwardMessageHistory(base, filter, forwardLimit)...)
page = append(page, backwardMessageHistory(base, filter, backwardLimit, true)...)
sort.SliceStable(page, func(i, j int) bool {
return messageLess(page[i], page[j])
})
return cloneMessages(page)
default:
start := filter.AddOffset
if start < 0 {
start = 0
}
candidates := backwardMessageHistory(base, filter, limit+start, false)
if start >= len(candidates) {
return nil
}
return cloneMessages(candidates[start:])
}
}
type messageHistoryLoad int
const (
messageHistoryLoadBackward messageHistoryLoad = iota
messageHistoryLoadForward
messageHistoryLoadAround
)
func messageHistoryLoadType(addOffset, limit int) messageHistoryLoad {
if addOffset >= 0 {
return messageHistoryLoadBackward
}
if addOffset+limit > 0 {
return messageHistoryLoadAround
}
return messageHistoryLoadForward
}
func backwardMessageHistory(base []domain.Message, filter domain.MessageFilter, limit int, includeOffset bool) []domain.Message {
if limit <= 0 {
return nil
}
out := make([]domain.Message, 0, limit)
for _, msg := range base {
if !messageBeforeHistoryOffset(msg, filter, includeOffset) {
continue
}
out = append(out, msg)
if len(out) == limit {
break
}
}
return out
}
func forwardMessageHistory(base []domain.Message, filter domain.MessageFilter, limit int) []domain.Message {
if limit <= 0 {
return nil
}
out := make([]domain.Message, 0, limit)
for i := len(base) - 1; i >= 0; i-- {
msg := base[i]
if !messageAfterHistoryOffset(msg, filter) {
continue
}
out = append(out, msg)
if len(out) == limit {
break
}
}
sort.SliceStable(out, func(i, j int) bool {
return messageLess(out[i], out[j])
})
return out
}
func messageBeforeHistoryOffset(msg domain.Message, filter domain.MessageFilter, includeOffset bool) bool {
if filter.OffsetDate > 0 {
if includeOffset {
return msg.Date <= filter.OffsetDate
}
return msg.Date < filter.OffsetDate
}
if filter.OffsetID <= 0 {
return true
}
if includeOffset {
return msg.ID <= filter.OffsetID
}
return msg.ID < filter.OffsetID
}
func messageAfterHistoryOffset(msg domain.Message, filter domain.MessageFilter) bool {
if filter.OffsetDate > 0 {
return msg.Date >= filter.OffsetDate
}
if filter.OffsetID <= 0 {
return false
}
return msg.ID > filter.OffsetID
}
func messageLess(a, b domain.Message) bool {
if a.Date != b.Date {
return a.Date > b.Date
}
return a.ID > b.ID
}
func usersForMessages(messages []domain.Message) []domain.User {
seen := map[int64]struct{}{}
users := make([]domain.User, 0, 1)
for _, msg := range messages {
for _, peer := range []domain.Peer{msg.Peer, msg.From} {
if peer.Type != domain.PeerTypeUser {
continue
}
if _, ok := seen[peer.ID]; ok {
continue
}
seen[peer.ID] = struct{}{}
if peer.ID == domain.OfficialSystemUserID {
users = append(users, domain.OfficialSystemUser())
}
}
}
return users
}
func messageListHash(messages []domain.Message) int64 {
if len(messages) == 0 {
return 0
}
h := fnv.New64a()
var buf [16]byte
for _, msg := range messages {
binary.LittleEndian.PutUint32(buf[:4], uint32(msg.ID))
binary.LittleEndian.PutUint32(buf[4:8], uint32(msg.Date))
binary.LittleEndian.PutUint64(buf[8:16], uint64(msg.From.ID))
_, _ = h.Write(buf[:])
writeMessageReactionsHash(h, msg.Reactions)
}
return int64(h.Sum64())
}
func writeMessageReactionsHash(h hash.Hash64, reactions *domain.ChannelMessageReactions) {
if reactions == nil {
_, _ = h.Write([]byte{0})
return
}
var buf [16]byte
for _, item := range reactions.Results {
_, _ = h.Write([]byte(item.Reaction.Type))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(item.Reaction.Emoticon))
_, _ = h.Write([]byte{0})
binary.LittleEndian.PutUint32(buf[:4], uint32(item.Count))
binary.LittleEndian.PutUint32(buf[4:8], uint32(item.ChosenOrder))
_, _ = h.Write(buf[:8])
}
_, _ = h.Write([]byte{0xfe})
for _, item := range reactions.Recent {
_, _ = h.Write([]byte(item.Reaction.Type))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(item.Reaction.Emoticon))
_, _ = h.Write([]byte{0})
binary.LittleEndian.PutUint64(buf[:8], uint64(item.UserID))
binary.LittleEndian.PutUint32(buf[8:12], uint32(item.Date))
binary.LittleEndian.PutUint32(buf[12:16], uint32(item.ChosenOrder))
_, _ = h.Write(buf[:])
}
}
func cloneContacts(contacts []domain.Contact) []domain.Contact {
out := append([]domain.Contact(nil), contacts...)
for i := range out {
out[i] = cloneContact(out[i])
}
return out
}
func cloneContact(contact domain.Contact) domain.Contact {
contact.NoteEntities = append([]domain.MessageEntity(nil), contact.NoteEntities...)
return contact
}
func contactListHash(contacts []domain.Contact) int64 {
if len(contacts) == 0 {
return 0
}
h := fnv.New64a()
var buf [16]byte
for _, contact := range contacts {
binary.LittleEndian.PutUint64(buf[:8], uint64(contact.User.ID))
if contact.Mutual {
buf[8] = 1
} else {
buf[8] = 0
}
_, _ = h.Write(buf[:9])
_, _ = h.Write([]byte(contact.FirstName))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(contact.LastName))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(contact.Phone))
_, _ = h.Write([]byte{0})
_, _ = h.Write([]byte(contact.Note))
_, _ = h.Write([]byte{0})
}
return int64(h.Sum64())
}
func filterDialogList(list domain.DialogList, filter domain.DialogFilter) domain.DialogList {
sort.SliceStable(list.Dialogs, func(i, j int) bool {
return dialogLess(list.Dialogs[i], list.Dialogs[j])
})
base := make([]domain.Dialog, 0, len(list.Dialogs))
for _, d := range list.Dialogs {
if !dialogMatchesFolder(d, list.Users, filter) {
continue
}
if filter.PinnedOnly && !d.Pinned {
continue
}
if filter.ExcludePinned && d.Pinned {
continue
}
base = append(base, d)
}
list.Count = len(base)
list.Hash = dialogListHash(base)
limit := filter.Limit
if limit <= 0 {
limit = 100
}
if limit > 500 {
limit = 500
}
page := make([]domain.Dialog, 0, len(base))
for _, d := range base {
if !afterDialogOffset(d, filter) {
continue
}
page = append(page, d)
if len(page) >= limit {
break
}
}
list.Dialogs = page
list.Messages = keepDialogMessages(list.Messages, page)
return list
}
func dialogMatchesFolder(d domain.Dialog, users []domain.User, filter domain.DialogFilter) bool {
if !filter.HasFolderID {
return true
}
if filter.FolderID < domain.DialogCustomFolderMinID {
return d.FolderID == filter.FolderID
}
if filter.Folder == nil {
return false
}
folder := filter.Folder
if folder.ExcludeArchived && d.FolderID == domain.DialogArchiveFolderID {
return false
}
if folder.ExcludeRead && d.UnreadCount == 0 && !d.UnreadMark {
return false
}
if hasFolderPeer(folder.ExcludePeers, d.Peer) {
return false
}
if hasFolderPeer(folder.IncludePeers, d.Peer) || hasFolderPeer(folder.PinnedPeers, d.Peer) {
return true
}
if d.Peer.Type == domain.PeerTypeUser {
user, ok := findDialogUser(users, d.Peer.ID)
if ok && user.Contact && folder.Contacts {
return true
}
if (!ok || !user.Contact) && folder.NonContacts {
return true
}
}
return false
}
func hasFolderPeer(peers []domain.DialogFolderPeer, peer domain.Peer) bool {
for _, item := range peers {
if item.Peer == peer {
return true
}
}
return false
}
func dialogLess(a, b domain.Dialog) bool {
if a.Pinned != b.Pinned {
return a.Pinned && !b.Pinned
}
if a.Pinned && b.Pinned && a.PinnedOrder != b.PinnedOrder {
if a.PinnedOrder == 0 {
return false
}
if b.PinnedOrder == 0 {
return true
}
return a.PinnedOrder < b.PinnedOrder
}
if a.TopMessageDate != b.TopMessageDate {
return a.TopMessageDate > b.TopMessageDate
}
if a.TopMessage != b.TopMessage {
return a.TopMessage > b.TopMessage
}
return a.Peer.ID > b.Peer.ID
}
func afterDialogOffset(d domain.Dialog, filter domain.DialogFilter) bool {
if filter.OffsetDate <= 0 && filter.OffsetID <= 0 {
return true
}
if filter.OffsetDate > 0 {
if d.TopMessageDate != filter.OffsetDate {
return d.TopMessageDate < filter.OffsetDate
}
if filter.OffsetID <= 0 {
return false
}
if d.TopMessage != filter.OffsetID {
return d.TopMessage < filter.OffsetID
}
if filter.HasOffsetPeer {
return d.Peer.ID < filter.OffsetPeer.ID
}
return false
}
return d.TopMessage < filter.OffsetID
}
func keepDialogMessages(messages []domain.Message, dialogs []domain.Dialog) []domain.Message {
want := make(map[int]struct{}, len(dialogs))
for _, d := range dialogs {
if d.TopMessage != 0 {
want[d.TopMessage] = struct{}{}
}
}
out := make([]domain.Message, 0, len(want))
for _, msg := range messages {
if _, ok := want[msg.ID]; ok {
out = append(out, msg)
}
}
return out
}
func findDialogUser(users []domain.User, id int64) (domain.User, bool) {
for _, user := range users {
if user.ID == id {
return user, true
}
}
return domain.User{}, false
}
func appendDialogUser(list *domain.DialogList, seen map[int64]struct{}, user domain.User) {
if user.ID == 0 {
return
}
if _, ok := seen[user.ID]; ok {
return
}
seen[user.ID] = struct{}{}
list.Users = append(list.Users, user)
}
func cloneMessages(messages []domain.Message) []domain.Message {
out := append([]domain.Message(nil), messages...)
for i := range out {
out[i] = cloneMessage(out[i])
}
return out
}
func cloneDialogs(dialogs []domain.Dialog) []domain.Dialog {
out := append([]domain.Dialog(nil), dialogs...)
for i := range out {
if out[i].Draft != nil {
draft := cloneDialogDraft(*out[i].Draft)
out[i].Draft = &draft
}
}
return out
}
func cloneDialogDraft(draft domain.DialogDraft) domain.DialogDraft {
draft.Entities = append([]domain.MessageEntity(nil), draft.Entities...)
draft.ReplyTo = cloneMessageReply(draft.ReplyTo)
if draft.WebPage != nil {
webpage := *draft.WebPage
draft.WebPage = &webpage
}
return draft
}
func draftKey(peer domain.Peer, topMessageID int) dialogDraftKey {
return dialogDraftKey{peerType: peer.Type, peerID: peer.ID, topMessageID: topMessageID}
}
func sortDialogDrafts(drafts []domain.DialogDraft) {
sort.SliceStable(drafts, func(i, j int) bool {
if drafts[i].Date != drafts[j].Date {
return drafts[i].Date > drafts[j].Date
}
if drafts[i].Peer.Type != drafts[j].Peer.Type {
return drafts[i].Peer.Type < drafts[j].Peer.Type
}
if drafts[i].Peer.ID != drafts[j].Peer.ID {
return drafts[i].Peer.ID > drafts[j].Peer.ID
}
return drafts[i].TopMessageID > drafts[j].TopMessageID
})
}
func cloneDialogFolder(folder domain.DialogFolder) domain.DialogFolder {
folder.TitleEntities = append([]domain.MessageEntity(nil), folder.TitleEntities...)
folder.PinnedPeers = append([]domain.DialogFolderPeer(nil), folder.PinnedPeers...)
folder.IncludePeers = append([]domain.DialogFolderPeer(nil), folder.IncludePeers...)
folder.ExcludePeers = append([]domain.DialogFolderPeer(nil), folder.ExcludePeers...)
return folder
}
func containsInt(items []int, value int) bool {
for _, item := range items {
if item == value {
return true
}
}
return false
}
func removeInt(items []int, value int) []int {
out := items[:0]
for _, item := range items {
if item != value {
out = append(out, item)
}
}
return out
}
func dialogListHash(dialogs []domain.Dialog) int64 {
if len(dialogs) == 0 {
return 0
}
h := fnv.New64a()
var buf [47]byte
for _, d := range dialogs {
binary.LittleEndian.PutUint64(buf[:8], uint64(d.Peer.ID))
binary.LittleEndian.PutUint32(buf[8:12], uint32(d.FolderID))
binary.LittleEndian.PutUint32(buf[12:16], uint32(d.TopMessage))
binary.LittleEndian.PutUint32(buf[16:20], uint32(d.TopMessageDate))
binary.LittleEndian.PutUint32(buf[20:24], uint32(d.ReadInboxMaxID))
binary.LittleEndian.PutUint32(buf[24:28], uint32(d.ReadOutboxMaxID))
binary.LittleEndian.PutUint32(buf[28:32], uint32(d.UnreadCount))
binary.LittleEndian.PutUint32(buf[32:36], uint32(d.UnreadMentions))
binary.LittleEndian.PutUint32(buf[36:40], uint32(d.UnreadReactions))
if d.Pinned {
buf[40] = 1
} else {
buf[40] = 0
}
binary.LittleEndian.PutUint32(buf[41:45], uint32(d.PinnedOrder))
if d.UnreadMark {
buf[45] = 1
} else {
buf[45] = 0
}
if d.PeerSettingsBarHidden {
buf[46] = 1
} else {
buf[46] = 0
}
_, _ = h.Write(buf[:])
}
return int64(h.Sum64())
}
// LangPackStore 是 store.LangPackStore 的内存实现。
type LangPackStore struct {
mu sync.RWMutex
m map[string]domain.LangPack
}
// NewLangPackStore 创建内存 LangPackStore。
func NewLangPackStore() *LangPackStore {
return &LangPackStore{m: make(map[string]domain.LangPack)}
}
func (s *LangPackStore) GetPack(_ context.Context, langPack, langCode string, fromVersion int) (domain.LangPack, error) {
s.mu.RLock()
pack := s.m[langPackKey(langPack, langCode)]
s.mu.RUnlock()
if pack.LangPack == "" {
return domain.LangPack{LangPack: langPack, LangCode: langCode, FromVersion: fromVersion}, nil
}
pack.FromVersion = fromVersion
if pack.Version <= fromVersion {
pack.Strings = nil
} else {
pack.Strings = append([]domain.LangPackString(nil), pack.Strings...)
}
return pack, nil
}
func (s *LangPackStore) GetStrings(_ context.Context, langPack, langCode string, keys []string) (domain.LangPack, error) {
s.mu.RLock()
pack := s.m[langPackKey(langPack, langCode)]
s.mu.RUnlock()
if pack.LangPack == "" {
return domain.LangPack{LangPack: langPack, LangCode: langCode}, nil
}
if len(keys) == 0 {
pack.Strings = append([]domain.LangPackString(nil), pack.Strings...)
return pack, nil
}
want := make(map[string]struct{}, len(keys))
for _, key := range keys {
want[key] = struct{}{}
}
out := domain.LangPack{LangPack: pack.LangPack, LangCode: pack.LangCode, Version: pack.Version}
for _, item := range pack.Strings {
if _, ok := want[item.Key]; ok {
out.Strings = append(out.Strings, item)
}
}
return out, nil
}
func (s *LangPackStore) UpsertPack(_ context.Context, pack domain.LangPack) error {
pack.Strings = append([]domain.LangPackString(nil), pack.Strings...)
s.mu.Lock()
s.m[langPackKey(pack.LangPack, pack.LangCode)] = pack
s.mu.Unlock()
return nil
}
func langPackKey(langPack, langCode string) string {
return langPack + "\x00" + langCode
}
// PasswordStore 是 store.PasswordStore 的内存实现。
type PasswordStore struct {
mu sync.RWMutex
m map[int64]domain.PasswordSettings
reactions map[int64]domain.AccountReactionSettings
}
// NewPasswordStore 创建内存 PasswordStore。
func NewPasswordStore() *PasswordStore {
return &PasswordStore{
m: make(map[int64]domain.PasswordSettings),
reactions: make(map[int64]domain.AccountReactionSettings),
}
}
func (s *PasswordStore) GetByUser(_ context.Context, userID int64) (domain.PasswordSettings, bool, error) {
s.mu.RLock()
settings, ok := s.m[userID]
s.mu.RUnlock()
settings.SecureRandom = append([]byte(nil), settings.SecureRandom...)
return settings, ok, nil
}
func (s *PasswordStore) Save(_ context.Context, userID int64, settings domain.PasswordSettings) error {
settings.SecureRandom = append([]byte(nil), settings.SecureRandom...)
s.mu.Lock()
s.m[userID] = settings
s.mu.Unlock()
return nil
}
func (s *PasswordStore) GetReactionSettings(_ context.Context, userID int64) (domain.AccountReactionSettings, bool, error) {
s.mu.RLock()
settings, ok := s.reactions[userID]
s.mu.RUnlock()
return cloneAccountReactionSettings(settings), ok, nil
}
func (s *PasswordStore) SaveReactionSettings(_ context.Context, userID int64, settings domain.AccountReactionSettings) error {
s.mu.Lock()
s.reactions[userID] = cloneAccountReactionSettings(settings)
s.mu.Unlock()
return nil
}
func cloneAccountReactionSettings(in domain.AccountReactionSettings) domain.AccountReactionSettings {
out := in
if in.PaidPrivacy.Peer != nil {
peer := *in.PaidPrivacy.Peer
out.PaidPrivacy.Peer = &peer
}
return out
}
// HelpStore 是 store.AppConfigStore 和 store.CountryStore 的内存实现。
type HelpStore struct {
mu sync.RWMutex
appConfig map[string]domain.AppConfig
countries domain.CountriesList
}
// NewHelpStore 创建内存 HelpStore。
func NewHelpStore() *HelpStore {
return &HelpStore{appConfig: make(map[string]domain.AppConfig)}
}
func (s *HelpStore) GetAppConfig(_ context.Context, client string) (domain.AppConfig, bool, error) {
s.mu.RLock()
cfg, ok := s.appConfig[client]
s.mu.RUnlock()
cfg.JSON = append([]byte(nil), cfg.JSON...)
return cfg, ok, nil
}
func (s *HelpStore) UpsertAppConfig(_ context.Context, cfg domain.AppConfig) error {
cfg.JSON = append([]byte(nil), cfg.JSON...)
s.mu.Lock()
s.appConfig[cfg.Client] = cfg
s.mu.Unlock()
return nil
}
func (s *HelpStore) ListCountries(_ context.Context, _ string) (domain.CountriesList, error) {
s.mu.RLock()
list := s.countries
s.mu.RUnlock()
list.Countries = append([]domain.Country(nil), list.Countries...)
for i := range list.Countries {
list.Countries[i].CountryCodes = append([]domain.CountryCode(nil), list.Countries[i].CountryCodes...)
}
return list, nil
}
func (s *HelpStore) UpsertCountries(_ context.Context, countries []domain.Country) error {
list := domain.CountriesList{Hash: 1, Countries: append([]domain.Country(nil), countries...)}
for i := range list.Countries {
list.Countries[i].CountryCodes = append([]domain.CountryCode(nil), list.Countries[i].CountryCodes...)
}
s.mu.Lock()
s.countries = list
s.mu.Unlock()
return nil
}
// UserStore 是 store.UserStore 的内存实现。ID 与 PG identity 使用同一业务起点。
type UserStore struct {
mu sync.RWMutex
byID map[int64]domain.User
nextID int64
}
// NewUserStore 创建内存 UserStore。
func NewUserStore() *UserStore {
return &UserStore{byID: make(map[int64]domain.User), nextID: domain.UserIDSequenceBase}
}
func (s *UserStore) ByID(_ context.Context, id int64) (domain.User, bool, error) {
s.mu.RLock()
u, ok := s.byID[id]
s.mu.RUnlock()
return u, ok, nil
}
func (s *UserStore) ByIDs(_ context.Context, ids []int64) ([]domain.User, error) {
if len(ids) == 0 {
return nil, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]domain.User, 0, len(ids))
seen := make(map[int64]struct{}, len(ids))
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
if u, ok := s.byID[id]; ok {
out = append(out, u)
}
}
return out, nil
}
func (s *UserStore) ByPhone(_ context.Context, phone string) (domain.User, bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, u := range s.byID {
if u.Phone == phone {
return u, true, nil
}
}
return domain.User{}, false, nil
}
func (s *UserStore) ByPhones(_ context.Context, phones []string) ([]domain.User, error) {
if len(phones) == 0 {
return nil, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
want := make(map[string]struct{}, len(phones))
for _, phone := range phones {
if phone != "" {
want[phone] = struct{}{}
}
}
out := make([]domain.User, 0, len(want))
seenIDs := map[int64]struct{}{}
for _, u := range s.byID {
if _, ok := want[u.Phone]; !ok {
continue
}
if _, ok := seenIDs[u.ID]; ok {
continue
}
seenIDs[u.ID] = struct{}{}
out = append(out, u)
}
sort.SliceStable(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out, nil
}
func (s *UserStore) ByUsername(_ context.Context, username string) (domain.User, bool, error) {
username = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(username, "@")))
if username == "" {
return domain.User{}, false, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
for _, u := range s.byID {
if strings.ToLower(u.Username) == username {
return u, true, nil
}
}
return domain.User{}, false, nil
}
func (s *UserStore) Search(_ context.Context, currentUserID int64, query, phoneQuery string, limit int) (domain.UserSearchResult, error) {
if limit <= 0 {
limit = 50
}
query = strings.ToLower(strings.TrimSpace(query))
phoneQuery = strings.TrimSpace(phoneQuery)
if query == "" {
return domain.UserSearchResult{}, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]domain.User, 0)
for _, u := range s.byID {
if u.ID == currentUserID {
continue
}
if userMatchesSearch(u, query, phoneQuery) {
users = append(users, u)
}
}
sort.SliceStable(users, func(i, j int) bool {
return users[i].ID < users[j].ID
})
if len(users) > limit {
users = users[:limit]
}
return domain.UserSearchResult{Results: users}, nil
}
func (s *UserStore) UpdateUsername(_ context.Context, userID int64, username string) (domain.User, error) {
username = strings.TrimSpace(strings.TrimPrefix(username, "@"))
usernameLower := strings.ToLower(username)
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return domain.User{}, domain.ErrUsernameNotOccupied
}
if usernameLower != "" {
for id, existing := range s.byID {
if id != userID && strings.ToLower(existing.Username) == usernameLower {
return domain.User{}, domain.ErrUsernameOccupied
}
}
}
u.Username = username
s.byID[userID] = u
return u, nil
}
func (s *UserStore) UpdateProfile(_ context.Context, userID int64, firstName, lastName, about string) (domain.User, error) {
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return domain.User{}, domain.ErrUsernameNotOccupied
}
u.FirstName = firstName
u.LastName = lastName
u.About = about
s.byID[userID] = u
return u, nil
}
func (s *UserStore) UpdateLastSeen(_ context.Context, userID int64, lastSeenAt int) error {
if lastSeenAt <= 0 {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
u, ok := s.byID[userID]
if !ok {
return domain.ErrUsernameNotOccupied
}
if lastSeenAt > u.LastSeenAt {
u.LastSeenAt = lastSeenAt
s.byID[userID] = u
}
return nil
}
func userMatchesSearch(u domain.User, query, phoneQuery string) bool {
if phoneQuery != "" && strings.HasPrefix(u.Phone, phoneQuery) {
return true
}
first := strings.ToLower(u.FirstName)
last := strings.ToLower(u.LastName)
username := strings.ToLower(u.Username)
fullName := strings.TrimSpace(first + " " + last)
return strings.Contains(first, query) ||
strings.Contains(last, query) ||
strings.Contains(fullName, query) ||
strings.Contains(username, query)
}
func (s *UserStore) Create(_ context.Context, u domain.User) (domain.User, error) {
s.mu.Lock()
defer s.mu.Unlock()
username := strings.ToLower(strings.TrimSpace(u.Username))
if username != "" {
for _, existing := range s.byID {
if strings.ToLower(existing.Username) == username {
return domain.User{}, domain.ErrUsernameOccupied
}
}
}
u.ID = s.nextID
s.nextID++
s.byID[u.ID] = u
return u, nil
}
// AuthorizationStore 是 store.AuthorizationStore 的内存实现。
type AuthorizationStore struct {
mu sync.RWMutex
m map[[8]byte]domain.Authorization
}
// NewAuthorizationStore 创建内存 AuthorizationStore。
func NewAuthorizationStore() *AuthorizationStore {
return &AuthorizationStore{m: make(map[[8]byte]domain.Authorization)}
}
func (s *AuthorizationStore) Bind(_ context.Context, a domain.Authorization) error {
s.mu.Lock()
s.m[a.AuthKeyID] = a
s.mu.Unlock()
return nil
}
func (s *AuthorizationStore) ByAuthKey(_ context.Context, id [8]byte) (domain.Authorization, bool, error) {
s.mu.RLock()
a, ok := s.m[id]
s.mu.RUnlock()
return a, ok, nil
}
func (s *AuthorizationStore) ListByUser(_ context.Context, userID int64) ([]domain.Authorization, error) {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]domain.Authorization, 0)
for _, a := range s.m {
if a.UserID == userID {
out = append(out, a)
}
}
return out, nil
}
func (s *AuthorizationStore) Delete(_ context.Context, id [8]byte) error {
s.mu.Lock()
delete(s.m, id)
s.mu.Unlock()
return nil
}
// CodeStore 是 store.CodeStore 的内存实现(带 TTL
type CodeStore struct {
mu sync.Mutex
m map[string]codeEntry
}
type codeEntry struct {
code store.PhoneCode
expires time.Time
}
// NewCodeStore 创建内存 CodeStore。
func NewCodeStore() *CodeStore {
return &CodeStore{m: make(map[string]codeEntry)}
}
func (s *CodeStore) Set(_ context.Context, hash string, code store.PhoneCode, ttl time.Duration) error {
s.mu.Lock()
s.m[hash] = codeEntry{code: code, expires: time.Now().Add(ttl)}
s.mu.Unlock()
return nil
}
func (s *CodeStore) Get(_ context.Context, hash string) (store.PhoneCode, bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
e, ok := s.m[hash]
if !ok || time.Now().After(e.expires) {
return store.PhoneCode{}, false, nil
}
return e.code, true, nil
}
func (s *CodeStore) Del(_ context.Context, hash string) error {
s.mu.Lock()
delete(s.m, hash)
s.mu.Unlock()
return nil
}