package postgres import ( "context" "encoding/binary" "errors" "fmt" "time" "github.com/jackc/pgx/v5" "telesrv/internal/domain" "telesrv/internal/store" "telesrv/internal/store/postgres/sqlcgen" ) // The two-int advisory-lock namespace is disjoint from the one-bigint user // locks used by lockUsersForUpdate. Only 32 digest bits are needed here: // collisions merely serialize unrelated deliveries and cannot merge receipts. const loginCodeDeliveryAdvisoryNamespace int32 = 0x4c434f44 // "LCOD" const ( loginCodeDeliveryRecoveryTimeout = 2 * time.Second loginCodeDeliveryRecoveryPoll = 20 * time.Millisecond ) type loginCodeDeliveryReceiptQuerier interface { QueryRow(ctx context.Context, sql string, args ...any) pgx.Row } type loginCodeDeliveryReceipt struct { userID int64 codeFingerprint []byte privateMessageID int64 messageBoxID int pts int messageDate int } // DeliverLoginCodeMessage commits the account-visible 777000 message, dialog // projection, user pts event, dispatch outbox row and compact idempotency // receipt in one transaction. The raw phone_code_hash is never persisted. func (s *MessageStore) DeliverLoginCodeMessage(ctx context.Context, req domain.LoginCodeDeliveryRequest) (domain.LoginCodeDeliveryResult, error) { 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("login code receipt expiry: %w: date=%d expires_at=%d", domain.ErrLoginCodeDeliveryInvalid, req.Date, req.ExpiresAt) } base, err := domain.OfficialLoginCodeMessage(req.UserID, req.Code, req.Date) if err != nil { return domain.LoginCodeDeliveryResult{}, err } entitiesJSON, err := encodeMessageEntities(base.Entities) if err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("encode login code entities: %w", err) } beginner, ok := s.db.(txBeginner) if !ok { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("deliver login code: database does not support transactions") } tx, err := beginner.Begin(ctx) if err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("begin login code delivery: %w", err) } committed := false defer func() { if !committed { rollbackCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), loginCodeDeliveryRecoveryTimeout) defer cancel() _ = tx.Rollback(rollbackCtx) } }() // Serialize the global idempotency key before any per-user row/advisory // lock. This makes same-key concurrent calls deterministic even if a caller // accidentally supplies a different user ID. lockKey := int32(binary.BigEndian.Uint32(deliveryKey[:4])) if _, err := tx.Exec(ctx, `SELECT pg_advisory_xact_lock($1::integer, $2::integer)`, loginCodeDeliveryAdvisoryNamespace, lockKey); err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("lock login code delivery: %w", err) } receipt, found, err := getLoginCodeDeliveryReceipt(ctx, tx, deliveryKey) if err != nil { return domain.LoginCodeDeliveryResult{}, err } if found { if receipt.userID != req.UserID || !store.SameLoginCodeFingerprint(receipt.codeFingerprint, codeFingerprint) { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("deliver login code replay: %w", domain.ErrLoginCodeDeliveryConflict) } msg, err := store.RestoreLoginCodeDeliveryMessage( receipt.userID, req.Code, receipt.messageDate, receipt.privateMessageID, receipt.messageBoxID, receipt.pts, ) if err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("restore login code replay: %w", err) } return domain.LoginCodeDeliveryResult{Message: msg, Created: false}, nil } // All user-scoped message/update writers share this lock and acquire it // before watermark/dialog rows, keeping box IDs and pts contiguous. if err := lockUsersForUpdate(ctx, tx, req.UserID); err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("lock login code recipient: %w", err) } if err := ensureOfficialSystemUserWithDB(ctx, tx, base); err != nil { return domain.LoginCodeDeliveryResult{}, err } qtx := sqlcgen.New(tx) pm, err := qtx.CreatePrivateMessage(ctx, sqlcgen.CreatePrivateMessageParams{ SenderUserID: domain.OfficialSystemUserID, RecipientUserID: req.UserID, RandomID: 0, MessageDate: int32(base.Date), Body: base.Body, RequestFingerprint: []byte{}, RecipientDelivered: true, EntitiesJson: entitiesJSON, QuoteEntitiesJson: []byte("[]"), MediaJson: []byte("{}"), ReplyMarkupJson: []byte("{}"), RichMessageJson: []byte("{}"), }) if err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("create login code private message: %w", err) } boxID, err := s.nextLoginCodeBoxID(ctx, qtx, req.UserID) if err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("allocate login code box id: %w", err) } if boxID <= 0 || boxID > domain.MaxMessageBoxID { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("allocate login code box id: %w: %d", domain.ErrLoginCodeDeliveryInvalid, boxID) } pts, err := s.reservePts(ctx, tx, req.UserID) if err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("allocate login code pts: %w", err) } boxRow, err := qtx.CreateMessageBox(ctx, sqlcgen.CreateMessageBoxParams{ OwnerUserID: req.UserID, BoxID: int32(boxID), PrivateMessageID: pm.ID, MessageSenderID: domain.OfficialSystemUserID, PeerType: string(domain.PeerTypeUser), PeerID: domain.OfficialSystemUserID, FromUserID: domain.OfficialSystemUserID, MessageDate: int32(base.Date), Outgoing: false, Body: base.Body, EntitiesJson: entitiesJSON, QuoteEntitiesJson: []byte("[]"), Pts: int32(pts), MediaJson: []byte("{}"), ReplyMarkupJson: []byte("{}"), RichMessageJson: []byte("{}"), }) if err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("create login code recipient box: %w", err) } msg := messageFromBoxRow(boxRow) if err := qtx.UpsertInboxDialog(ctx, sqlcgen.UpsertInboxDialogParams{ UserID: req.UserID, PeerType: string(domain.PeerTypeUser), PeerID: domain.OfficialSystemUserID, TopMessageID: int32(msg.ID), TopMessageDate: int32(msg.Date), }); err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("upsert login code dialog: %w", err) } if err := appendNewMessageEvent(ctx, qtx, msg); err != nil { return domain.LoginCodeDeliveryResult{}, err } if err := enqueueDispatch(ctx, qtx, sqlcgen.EnqueueDispatchParams{ TargetUserID: req.UserID, Pts: int32(msg.Pts), EventType: string(domain.UpdateEventNewMessage), ExcludeAuthKeyID: 0, ExcludeSessionID: 0, }); err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("enqueue login code dispatch: %w", err) } tag, err := tx.Exec(ctx, ` UPDATE private_messages SET recipient_box_id = $3, recipient_pts = $4 WHERE sender_user_id = $1 AND id = $2 AND recipient_delivered AND recipient_box_id = 0 AND recipient_pts = 0`, domain.OfficialSystemUserID, pm.ID, msg.ID, msg.Pts) if err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("save login code private receipt: %w", err) } if tag.RowsAffected() != 1 { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("save login code private receipt: message %d lost its allocation boundary", pm.ID) } if _, err := tx.Exec(ctx, ` INSERT INTO login_code_message_deliveries ( delivery_key, code_fingerprint, user_id, private_message_id, message_box_id, pts, message_date, expires_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, deliveryKey[:], codeFingerprint[:], req.UserID, msg.UID, msg.ID, msg.Pts, msg.Date, time.Unix(req.ExpiresAt, 0).UTC(), ); err != nil { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("save login code delivery receipt: %w", err) } if err := tx.Commit(ctx); err != nil { result, recoverErr := s.recoverLoginCodeDeliveryAfterCommitError(ctx, req, deliveryKey, codeFingerprint) if recoverErr != nil { return domain.LoginCodeDeliveryResult{}, errors.Join( fmt.Errorf("commit login code delivery: %w", err), recoverErr, ) } committed = true return result, nil } committed = true return domain.LoginCodeDeliveryResult{Message: msg, Created: true}, nil } func (s *MessageStore) recoverLoginCodeDeliveryAfterCommitError( ctx context.Context, req domain.LoginCodeDeliveryRequest, deliveryKey, codeFingerprint [32]byte, ) (domain.LoginCodeDeliveryResult, error) { probeCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), loginCodeDeliveryRecoveryTimeout) defer cancel() ticker := time.NewTicker(loginCodeDeliveryRecoveryPoll) defer ticker.Stop() for { receipt, found, err := getLoginCodeDeliveryReceipt(probeCtx, s.db, deliveryKey) if err != nil { return domain.LoginCodeDeliveryResult{}, errors.Join( domain.ErrLoginCodeDeliveryCommitAmbiguous, fmt.Errorf("probe login code delivery receipt after commit error: %w", err), ) } if found { if receipt.userID != req.UserID || !store.SameLoginCodeFingerprint(receipt.codeFingerprint, codeFingerprint) { return domain.LoginCodeDeliveryResult{}, fmt.Errorf("probe login code delivery receipt after commit error: %w", domain.ErrLoginCodeDeliveryConflict) } msg, err := store.RestoreLoginCodeDeliveryMessage( receipt.userID, req.Code, receipt.messageDate, receipt.privateMessageID, receipt.messageBoxID, receipt.pts, ) if err != nil { return domain.LoginCodeDeliveryResult{}, errors.Join( domain.ErrLoginCodeDeliveryCommitAmbiguous, fmt.Errorf("restore probed login code delivery: %w", err), ) } // The receipt proves durable success but cannot prove whether this // caller or an equivalent replay won the commit race. return domain.LoginCodeDeliveryResult{Message: msg, Created: false}, nil } select { case <-probeCtx.Done(): return domain.LoginCodeDeliveryResult{}, errors.Join( domain.ErrLoginCodeDeliveryCommitAmbiguous, fmt.Errorf("probe login code delivery receipt after commit error: %w", probeCtx.Err()), ) case <-ticker.C: } } } func getLoginCodeDeliveryReceipt(ctx context.Context, q loginCodeDeliveryReceiptQuerier, deliveryKey [32]byte) (loginCodeDeliveryReceipt, bool, error) { var receipt loginCodeDeliveryReceipt var boxID, pts, messageDate int32 err := q.QueryRow(ctx, ` SELECT user_id, code_fingerprint, private_message_id, message_box_id, pts, message_date FROM login_code_message_deliveries WHERE delivery_key = $1`, deliveryKey[:]).Scan( &receipt.userID, &receipt.codeFingerprint, &receipt.privateMessageID, &boxID, &pts, &messageDate, ) if errors.Is(err, pgx.ErrNoRows) { return loginCodeDeliveryReceipt{}, false, nil } if err != nil { return loginCodeDeliveryReceipt{}, false, fmt.Errorf("load login code delivery receipt: %w", err) } receipt.messageBoxID = int(boxID) receipt.pts = int(pts) receipt.messageDate = int(messageDate) return receipt, true, nil } func (s *MessageStore) nextLoginCodeBoxID(ctx context.Context, qtx *sqlcgen.Queries, userID int64) (int, error) { // The default allocator queries PostgreSQL. Run that query on the active // transaction connection: querying s.q while holding the transaction can // deadlock a MaxConns=1 pool. External allocators (Redis/counters) retain // their normal semantics. switch s.boxIDs.(type) { case pgBoxIDAllocator, *pgBoxIDAllocator: current, err := qtx.MaxMessageBoxID(ctx, userID) if err != nil { return 0, err } return int(current) + 1, nil default: return s.boxIDs.NextBoxID(ctx, userID) } }