chore: refresh gramsrv public release
This commit is contained in:
parent
75cebe8dbf
commit
70b6820474
1274 changed files with 378751 additions and 59919 deletions
193
internal/store/postgres/secretchat.go
Normal file
193
internal/store/postgres/secretchat.go
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/postgres/sqlcgen"
|
||||
)
|
||||
|
||||
// SecretChatStore 是 store.SecretChatStore 的 PostgreSQL 实现(迁移 0137)。
|
||||
// 盲中继:g_a/g_b/key_fingerprint 原样 BYTEA/BIGINT 存储;握手态迁移用条件
|
||||
// UPDATE 做原子 CAS(accept 绑定接受设备、discard 幂等)。行为契约与 memory
|
||||
// 实现由 storetest 钉死。
|
||||
type SecretChatStore struct {
|
||||
db sqlcgen.DBTX
|
||||
}
|
||||
|
||||
// NewSecretChatStore 基于 pgx 连接池创建 SecretChatStore。
|
||||
func NewSecretChatStore(db sqlcgen.DBTX) *SecretChatStore {
|
||||
return &SecretChatStore{db: db}
|
||||
}
|
||||
|
||||
const secretChatColumns = `chat_id, admin_access_hash, participant_access_hash, admin_user_id,
|
||||
admin_auth_key_id, participant_user_id, participant_auth_key_id, state, g_a, g_b,
|
||||
key_fingerprint, layer, folder_id, history_deleted, random_id, date`
|
||||
|
||||
func scanSecretChat(row rowScanner) (domain.SecretChat, error) {
|
||||
var c domain.SecretChat
|
||||
var state string
|
||||
if err := row.Scan(
|
||||
&c.ID, &c.AdminAccessHash, &c.ParticipantAccessHash, &c.AdminUserID,
|
||||
&c.AdminAuthKeyID, &c.ParticipantUserID, &c.ParticipantAuthKeyID, &state, &c.GA, &c.GB,
|
||||
&c.KeyFingerprint, &c.Layer, &c.FolderID, &c.HistoryDeleted, &c.RandomID, &c.Date,
|
||||
); err != nil {
|
||||
return domain.SecretChat{}, err
|
||||
}
|
||||
c.State = domain.SecretChatState(state)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *SecretChatStore) CreateSecretChat(ctx context.Context, chat domain.SecretChat) error {
|
||||
if chat.ID == 0 {
|
||||
return domain.ErrSecretChatNotFound
|
||||
}
|
||||
if chat.State == "" {
|
||||
chat.State = domain.SecretChatStateRequested
|
||||
}
|
||||
_, err := s.db.Exec(ctx, `
|
||||
INSERT INTO secret_chats (chat_id, admin_access_hash, participant_access_hash, admin_user_id,
|
||||
admin_auth_key_id, participant_user_id, participant_auth_key_id, state, g_a, g_b,
|
||||
key_fingerprint, layer, folder_id, history_deleted, random_id, date)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`,
|
||||
chat.ID, chat.AdminAccessHash, chat.ParticipantAccessHash, chat.AdminUserID,
|
||||
chat.AdminAuthKeyID, chat.ParticipantUserID, chat.ParticipantAuthKeyID, string(chat.State),
|
||||
nullableBytes(chat.GA), nullableBytes(chat.GB), chat.KeyFingerprint, chat.Layer,
|
||||
chat.FolderID, chat.HistoryDeleted, chat.RandomID, chat.Date)
|
||||
if err != nil {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
if pgErr.ConstraintName == "secret_chats_pkey" {
|
||||
return domain.ErrSecretChatIDConflict
|
||||
}
|
||||
// uq_secret_chats_admin_random:与并发同 random_id 请求竞态(罕见,
|
||||
// 服务端 GetByAdminRandom 预检已收口大多数情况)。
|
||||
return fmt.Errorf("insert secret chat: duplicate admin random: %w", err)
|
||||
}
|
||||
return fmt.Errorf("insert secret chat: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SecretChatStore) GetSecretChat(ctx context.Context, chatID int) (domain.SecretChat, bool, error) {
|
||||
chat, err := scanSecretChat(s.db.QueryRow(ctx,
|
||||
`SELECT `+secretChatColumns+` FROM secret_chats WHERE chat_id = $1`, chatID))
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.SecretChat{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.SecretChat{}, false, fmt.Errorf("get secret chat: %w", err)
|
||||
}
|
||||
return chat, true, nil
|
||||
}
|
||||
|
||||
func (s *SecretChatStore) GetByAdminRandom(ctx context.Context, adminAuthKeyID int64, randomID int32) (domain.SecretChat, bool, error) {
|
||||
chat, err := scanSecretChat(s.db.QueryRow(ctx,
|
||||
`SELECT `+secretChatColumns+` FROM secret_chats
|
||||
WHERE admin_auth_key_id = $1 AND random_id = $2 AND state <> 'discarded' LIMIT 1`,
|
||||
adminAuthKeyID, randomID))
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.SecretChat{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.SecretChat{}, false, fmt.Errorf("get secret chat by admin random: %w", err)
|
||||
}
|
||||
return chat, true, nil
|
||||
}
|
||||
|
||||
func (s *SecretChatStore) AcceptSecretChat(ctx context.Context, chatID int, participantAuthKeyID int64, gb []byte, keyFingerprint int64) (domain.SecretChat, error) {
|
||||
// 原子 CAS:仅 requested 且未绑定接受设备时迁移到 normal。
|
||||
chat, err := scanSecretChat(s.db.QueryRow(ctx, `
|
||||
UPDATE secret_chats
|
||||
SET state = 'normal', participant_auth_key_id = $2, g_b = $3, key_fingerprint = $4
|
||||
WHERE chat_id = $1 AND state = 'requested' AND participant_auth_key_id = 0
|
||||
RETURNING `+secretChatColumns, chatID, participantAuthKeyID, nullableBytes(gb), keyFingerprint))
|
||||
if err == nil {
|
||||
return chat, nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.SecretChat{}, fmt.Errorf("accept secret chat: %w", err)
|
||||
}
|
||||
// CAS 落空:按当前态决定错误码。
|
||||
current, ok, gerr := s.GetSecretChat(ctx, chatID)
|
||||
if gerr != nil {
|
||||
return domain.SecretChat{}, gerr
|
||||
}
|
||||
if !ok {
|
||||
return domain.SecretChat{}, domain.ErrSecretChatNotFound
|
||||
}
|
||||
switch current.State {
|
||||
case domain.SecretChatStateDiscarded:
|
||||
return domain.SecretChat{}, domain.ErrSecretChatAlreadyDeclined
|
||||
default:
|
||||
return domain.SecretChat{}, domain.ErrSecretChatAlreadyAccepted
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SecretChatStore) DiscardSecretChat(ctx context.Context, chatID int, historyDeleted bool) (domain.SecretChat, bool, error) {
|
||||
chat, err := scanSecretChat(s.db.QueryRow(ctx, `
|
||||
UPDATE secret_chats
|
||||
SET state = 'discarded', history_deleted = $2
|
||||
WHERE chat_id = $1 AND state <> 'discarded'
|
||||
RETURNING `+secretChatColumns, chatID, historyDeleted))
|
||||
if err == nil {
|
||||
return chat, false, nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.SecretChat{}, false, fmt.Errorf("discard secret chat: %w", err)
|
||||
}
|
||||
// 未更新:要么已是终态(幂等成功),要么不存在。
|
||||
current, ok, gerr := s.GetSecretChat(ctx, chatID)
|
||||
if gerr != nil {
|
||||
return domain.SecretChat{}, false, gerr
|
||||
}
|
||||
if !ok {
|
||||
return domain.SecretChat{}, false, domain.ErrSecretChatNotFound
|
||||
}
|
||||
return current, true, nil
|
||||
}
|
||||
|
||||
func (s *SecretChatStore) ListActiveSecretChatsByAuthKey(ctx context.Context, authKeyID int64) ([]domain.SecretChat, error) {
|
||||
if authKeyID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `SELECT `+secretChatColumns+` FROM secret_chats
|
||||
WHERE state <> 'discarded' AND (admin_auth_key_id = $1 OR participant_auth_key_id = $1)
|
||||
ORDER BY chat_id`, authKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list secret chats by auth key: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []domain.SecretChat
|
||||
for rows.Next() {
|
||||
chat, serr := scanSecretChat(rows)
|
||||
if serr != nil {
|
||||
return nil, fmt.Errorf("scan secret chat by auth key: %w", serr)
|
||||
}
|
||||
out = append(out, chat)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("list secret chats by auth key rows: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *SecretChatStore) MaxSecretChatID(ctx context.Context) (int, error) {
|
||||
var id int
|
||||
if err := s.db.QueryRow(ctx, `SELECT COALESCE(MAX(chat_id), 0) FROM secret_chats`).Scan(&id); err != nil {
|
||||
return 0, fmt.Errorf("max secret chat id: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func nullableBytes(b []byte) any {
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue