owpengram-server/internal/store/memory/login_code_delivery.go
2026-09-01 14:40:06 +03:00

130 lines
4.8 KiB
Go

package memory
import (
"context"
"fmt"
"time"
"telesrv/internal/domain"
"telesrv/internal/store"
)
type loginCodeDeliveryRecord struct {
userID int64
codeFingerprint [32]byte
privateMessageID int64
messageBoxID int
pts int
messageDate int
}
// LoginCodeDeliveryStore composes the message projection with the same
// durable update-event store consumed by updates.getDifference. Keeping this
// as an explicit dependency prevents tests (and future in-memory runtimes)
// from accidentally creating a visible message without its pts event.
type LoginCodeDeliveryStore struct {
messages *MessageStore
events *UpdateEventStore
}
func NewLoginCodeDeliveryStore(messages *MessageStore, events *UpdateEventStore) *LoginCodeDeliveryStore {
return &LoginCodeDeliveryStore{messages: messages, events: events}
}
// DeliverLoginCodeMessage is the in-memory equivalent of the PostgreSQL
// transaction. Message, dialog, pts, durable event and immutable receipt are
// published under the two backing stores' locks; a repeated phone_code_hash
// returns the first snapshot.
func (s *LoginCodeDeliveryStore) DeliverLoginCodeMessage(_ context.Context, req domain.LoginCodeDeliveryRequest) (domain.LoginCodeDeliveryResult, error) {
if s == nil || s.messages == nil || s.events == nil {
return domain.LoginCodeDeliveryResult{}, fmt.Errorf("memory login code delivery: %w: message and update stores are required", domain.ErrLoginCodeDeliveryInvalid)
}
deliveryKey, err := store.LoginCodeDeliveryKey(req.PhoneCodeHash)
if err != nil {
return domain.LoginCodeDeliveryResult{}, err
}
codeFingerprint, err := store.LoginCodeFingerprint(req.PhoneCodeHash, req.Code)
if err != nil {
return domain.LoginCodeDeliveryResult{}, err
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
if req.ExpiresAt <= int64(req.Date) {
return domain.LoginCodeDeliveryResult{}, fmt.Errorf("memory login code receipt expiry: %w: date=%d expires_at=%d", domain.ErrLoginCodeDeliveryInvalid, req.Date, req.ExpiresAt)
}
base, err := domain.OfficialLoginCodeMessage(req.UserID, req.Template, req.Code, req.Date)
if err != nil {
return domain.LoginCodeDeliveryResult{}, err
}
// Lock ordering is local and fixed: MessageStore -> UpdateEventStore ->
// DialogStore. No other memory operation takes the first two together.
s.messages.mu.Lock()
defer s.messages.mu.Unlock()
s.events.mu.Lock()
defer s.events.mu.Unlock()
if receipt, ok := s.messages.loginCodeDeliveries[deliveryKey]; ok {
if receipt.userID != req.UserID || !store.SameLoginCodeFingerprint(receipt.codeFingerprint[:], codeFingerprint) {
return domain.LoginCodeDeliveryResult{}, fmt.Errorf("memory login code delivery: %w", domain.ErrLoginCodeDeliveryConflict)
}
msg, err := store.RestoreLoginCodeDeliveryMessage(
receipt.userID,
req.Template,
req.Code,
receipt.messageDate,
receipt.privateMessageID,
receipt.messageBoxID,
receipt.pts,
)
if err != nil {
return domain.LoginCodeDeliveryResult{}, fmt.Errorf("memory login code delivery replay: %w", err)
}
return domain.LoginCodeDeliveryResult{Message: msg, Created: false}, nil
}
currentEventPts := 0
for _, event := range s.events.events[req.UserID] {
if event.Pts > currentEventPts {
currentEventPts = event.Pts
}
}
if messagePts := s.messages.nextPts[req.UserID]; messagePts > currentEventPts {
return domain.LoginCodeDeliveryResult{}, fmt.Errorf("memory login code delivery: %w: message pts %d exceeds durable event pts %d", domain.ErrLoginCodeDeliveryInvalid, messagePts, currentEventPts)
}
base.ID = s.messages.nextBoxIDLocked(req.UserID)
base.UID = s.messages.nextUID
s.messages.nextUID++
base.Pts = currentEventPts + 1
s.messages.nextPts[req.UserID] = base.Pts
s.messages.m[req.UserID] = append(s.messages.m[req.UserID], cloneMessage(base))
if s.messages.dialogs != nil {
s.messages.dialogs.mu.Lock()
list := s.messages.dialogs.m[req.UserID]
list = upsertMemoryDialog(list, domain.Dialog{
Peer: base.Peer,
TopMessage: base.ID,
TopMessageDate: base.Date,
UnreadCount: s.messages.privateUnreadCountLocked(req.UserID, base.Peer),
})
if !hasUser(list.Users, domain.OfficialSystemUserID) {
list.Users = append(list.Users, domain.OfficialSystemUser())
}
list.Messages = append(list.Messages, cloneMessage(base))
s.messages.dialogs.m[req.UserID] = list
s.messages.dialogs.mu.Unlock()
}
event := newMessageEvent(base)
s.events.events[req.UserID] = append(s.events.events[req.UserID], cloneUpdateEvent(event))
s.messages.loginCodeDeliveries[deliveryKey] = loginCodeDeliveryRecord{
userID: req.UserID,
codeFingerprint: codeFingerprint,
privateMessageID: base.UID,
messageBoxID: base.ID,
pts: base.Pts,
messageDate: base.Date,
}
return domain.LoginCodeDeliveryResult{Message: cloneMessage(base), Created: true}, nil
}