chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
282
internal/store/postgres/encrypted_queue.go
Normal file
282
internal/store/postgres/encrypted_queue.go
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
// EncryptedQueueStore 是 store.EncryptedQueueStore 的 PostgreSQL 实现(迁移 0138)。
|
||||
// 盲中继 qts 投递队列:qts 分配(secret_qts_watermarks.reserved_qts 自增)+ 写队列行
|
||||
// 在单事务内完成,保证设备 qts 无空洞。bytes 原样 BYTEA 存储,永不解密。
|
||||
type EncryptedQueueStore struct {
|
||||
db sqlcgen.DBTX
|
||||
}
|
||||
|
||||
// NewEncryptedQueueStore 基于 pgx 连接池创建 EncryptedQueueStore。
|
||||
func NewEncryptedQueueStore(db sqlcgen.DBTX) *EncryptedQueueStore {
|
||||
return &EncryptedQueueStore{db: db}
|
||||
}
|
||||
|
||||
const encryptedMessageColumns = `receiver_auth_key_id, qts, receiver_user_id, chat_id, random_id,
|
||||
date, is_service, bytes, file_id, file_access_hash, file_size, file_dc_id, file_key_fingerprint`
|
||||
|
||||
func scanEncryptedMessage(row rowScanner) (domain.SecretChatMessage, error) {
|
||||
var m domain.SecretChatMessage
|
||||
var (
|
||||
fileID, fileAccessHash, fileSize *int64
|
||||
fileDC, fileKeyFP *int32
|
||||
)
|
||||
if err := row.Scan(
|
||||
&m.ReceiverAuthKeyID, &m.Qts, &m.ReceiverUserID, &m.ChatID, &m.RandomID,
|
||||
&m.Date, &m.IsService, &m.Bytes, &fileID, &fileAccessHash, &fileSize, &fileDC, &fileKeyFP,
|
||||
); err != nil {
|
||||
return domain.SecretChatMessage{}, err
|
||||
}
|
||||
if fileID != nil {
|
||||
ref := domain.EncryptedFileRef{ID: *fileID}
|
||||
if fileAccessHash != nil {
|
||||
ref.AccessHash = *fileAccessHash
|
||||
}
|
||||
if fileSize != nil {
|
||||
ref.Size = *fileSize
|
||||
}
|
||||
if fileDC != nil {
|
||||
ref.DCID = int(*fileDC)
|
||||
}
|
||||
if fileKeyFP != nil {
|
||||
ref.KeyFingerprint = int(*fileKeyFP)
|
||||
}
|
||||
m.File = &ref
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) begin(ctx context.Context, op string) (pgx.Tx, error) {
|
||||
beginner, ok := s.db.(txBeginner)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s: db does not support transactions", op)
|
||||
}
|
||||
tx, err := beginner.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin %s: %w", op, err)
|
||||
}
|
||||
return tx, nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) AppendEncryptedMessage(ctx context.Context, msg domain.SecretChatMessage) (domain.SecretChatMessage, bool, error) {
|
||||
tx, err := s.begin(ctx, "append encrypted message")
|
||||
if err != nil {
|
||||
return domain.SecretChatMessage{}, false, err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
// 幂等:同接收设备同 chat 同 random_id 已存在 → 返回既有行(不重分配 qts)。
|
||||
existing, err := scanEncryptedMessage(tx.QueryRow(ctx,
|
||||
`SELECT `+encryptedMessageColumns+` FROM encrypted_message_queue
|
||||
WHERE receiver_auth_key_id = $1 AND chat_id = $2 AND random_id = $3`,
|
||||
msg.ReceiverAuthKeyID, msg.ChatID, msg.RandomID))
|
||||
if err == nil {
|
||||
if cerr := tx.Commit(ctx); cerr != nil {
|
||||
return domain.SecretChatMessage{}, false, fmt.Errorf("commit append (dedup): %w", cerr)
|
||||
}
|
||||
committed = true
|
||||
return existing, true, nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.SecretChatMessage{}, false, fmt.Errorf("dedup lookup: %w", err)
|
||||
}
|
||||
|
||||
// 分配下一个 qts(首值 1),与写队列同事务。
|
||||
var qts int
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO secret_qts_watermarks (auth_key_id, reserved_qts)
|
||||
VALUES ($1, 1)
|
||||
ON CONFLICT (auth_key_id) DO UPDATE SET reserved_qts = secret_qts_watermarks.reserved_qts + 1, updated_at = now()
|
||||
RETURNING reserved_qts`, msg.ReceiverAuthKeyID).Scan(&qts); err != nil {
|
||||
return domain.SecretChatMessage{}, false, fmt.Errorf("reserve device qts: %w", err)
|
||||
}
|
||||
msg.Qts = qts
|
||||
|
||||
var (
|
||||
fileID, fileAccessHash, fileSize any
|
||||
fileDC, fileKeyFP any
|
||||
)
|
||||
if msg.File != nil {
|
||||
fileID = msg.File.ID
|
||||
fileAccessHash = msg.File.AccessHash
|
||||
fileSize = msg.File.Size
|
||||
fileDC = int32(msg.File.DCID)
|
||||
fileKeyFP = int32(msg.File.KeyFingerprint)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO encrypted_message_queue (receiver_auth_key_id, qts, receiver_user_id, chat_id, random_id,
|
||||
date, is_service, bytes, file_id, file_access_hash, file_size, file_dc_id, file_key_fingerprint)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
|
||||
msg.ReceiverAuthKeyID, msg.Qts, msg.ReceiverUserID, msg.ChatID, msg.RandomID,
|
||||
msg.Date, msg.IsService, msg.Bytes, fileID, fileAccessHash, fileSize, fileDC, fileKeyFP); err != nil {
|
||||
return domain.SecretChatMessage{}, false, fmt.Errorf("insert encrypted message: %w", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return domain.SecretChatMessage{}, false, fmt.Errorf("commit append encrypted message: %w", err)
|
||||
}
|
||||
committed = true
|
||||
return msg, false, nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) ListEncryptedMessagesSince(ctx context.Context, receiverAuthKeyID int64, sinceQts, limit int) ([]domain.SecretChatMessage, error) {
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
rows, err := s.db.Query(ctx,
|
||||
`SELECT `+encryptedMessageColumns+` FROM encrypted_message_queue
|
||||
WHERE receiver_auth_key_id = $1 AND qts > $2 ORDER BY qts ASC LIMIT $3`,
|
||||
receiverAuthKeyID, sinceQts, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list encrypted messages: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []domain.SecretChatMessage
|
||||
for rows.Next() {
|
||||
m, err := scanEncryptedMessage(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) ReservedQts(ctx context.Context, receiverAuthKeyID int64) (int, error) {
|
||||
var qts int
|
||||
err := s.db.QueryRow(ctx,
|
||||
`SELECT reserved_qts FROM secret_qts_watermarks WHERE auth_key_id = $1`, receiverAuthKeyID).Scan(&qts)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("reserved qts: %w", err)
|
||||
}
|
||||
return qts, nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) AckEncryptedMessages(ctx context.Context, receiverAuthKeyID int64, maxQts int) error {
|
||||
if maxQts <= 0 {
|
||||
return nil
|
||||
}
|
||||
// 推进 confirmed_qts(GREATEST 幂等,回退忽略)。
|
||||
if _, err := s.db.Exec(ctx, `
|
||||
INSERT INTO secret_qts_watermarks (auth_key_id, confirmed_qts)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (auth_key_id) DO UPDATE SET confirmed_qts = GREATEST(secret_qts_watermarks.confirmed_qts, EXCLUDED.confirmed_qts), updated_at = now()`,
|
||||
receiverAuthKeyID, maxQts); err != nil {
|
||||
return fmt.Errorf("advance confirmed qts: %w", err)
|
||||
}
|
||||
if _, err := s.db.Exec(ctx, `
|
||||
UPDATE encrypted_message_queue SET acked = true
|
||||
WHERE receiver_auth_key_id = $1 AND qts <= $2 AND NOT acked`, receiverAuthKeyID, maxQts); err != nil {
|
||||
return fmt.Errorf("ack encrypted messages: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) AppendStateEvent(ctx context.Context, ev domain.EncryptedStateEvent) (int64, error) {
|
||||
var id int64
|
||||
if err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO encrypted_state_events (target_user_id, target_auth_key_id, chat_id, event_type, max_date, date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id`,
|
||||
ev.TargetUserID, ev.TargetAuthKeyID, ev.ChatID, int16(ev.Type), ev.MaxDate, ev.Date).Scan(&id); err != nil {
|
||||
return 0, fmt.Errorf("append state event: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) ListUndeliveredStateEvents(ctx context.Context, targetUserID, deviceAuthKeyID int64, limit int) ([]domain.EncryptedStateEvent, error) {
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT e.id, e.target_user_id, e.target_auth_key_id, e.chat_id, e.event_type, e.max_date, e.date
|
||||
FROM encrypted_state_events e
|
||||
WHERE e.target_user_id = $1
|
||||
AND (e.target_auth_key_id = 0 OR e.target_auth_key_id = $2)
|
||||
AND NOT EXISTS (SELECT 1 FROM encrypted_state_event_delivery d WHERE d.event_id = e.id AND d.auth_key_id = $2)
|
||||
ORDER BY e.id ASC LIMIT $3`, targetUserID, deviceAuthKeyID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list undelivered state events: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []domain.EncryptedStateEvent
|
||||
for rows.Next() {
|
||||
var ev domain.EncryptedStateEvent
|
||||
var typ int16
|
||||
if err := rows.Scan(&ev.ID, &ev.TargetUserID, &ev.TargetAuthKeyID, &ev.ChatID, &typ, &ev.MaxDate, &ev.Date); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ev.Type = domain.EncryptedStateEventType(typ)
|
||||
out = append(out, ev)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) MarkStateEventsDelivered(ctx context.Context, deviceAuthKeyID int64, eventIDs []int64) error {
|
||||
if len(eventIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := s.db.Exec(ctx, `
|
||||
INSERT INTO encrypted_state_event_delivery (event_id, auth_key_id)
|
||||
SELECT unnest($1::bigint[]), $2
|
||||
ON CONFLICT (event_id, auth_key_id) DO NOTHING`, eventIDs, deviceAuthKeyID); err != nil {
|
||||
return fmt.Errorf("mark state events delivered: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) PutEncryptedFile(ctx context.Context, ownerUserID int64, ref domain.EncryptedFileRef) error {
|
||||
if _, err := s.db.Exec(ctx, `
|
||||
INSERT INTO encrypted_files (id, access_hash, owner_user_id, size, dc_id, key_fingerprint)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (id) DO UPDATE SET access_hash = EXCLUDED.access_hash, size = EXCLUDED.size,
|
||||
dc_id = EXCLUDED.dc_id, key_fingerprint = EXCLUDED.key_fingerprint`,
|
||||
ref.ID, ref.AccessHash, ownerUserID, ref.Size, int32(ref.DCID), int32(ref.KeyFingerprint)); err != nil {
|
||||
return fmt.Errorf("put encrypted file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EncryptedQueueStore) GetEncryptedFile(ctx context.Context, id, accessHash int64) (domain.EncryptedFileRef, bool, error) {
|
||||
var (
|
||||
ref domain.EncryptedFileRef
|
||||
dc int32
|
||||
keyFP int32
|
||||
)
|
||||
err := s.db.QueryRow(ctx,
|
||||
`SELECT id, access_hash, size, dc_id, key_fingerprint FROM encrypted_files WHERE id = $1 AND access_hash = $2`,
|
||||
id, accessHash).Scan(&ref.ID, &ref.AccessHash, &ref.Size, &dc, &keyFP)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.EncryptedFileRef{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.EncryptedFileRef{}, false, fmt.Errorf("get encrypted file: %w", err)
|
||||
}
|
||||
ref.DCID = int(dc)
|
||||
ref.KeyFingerprint = int(keyFP)
|
||||
return ref, true, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue