feat: sync account deletion lifecycle
Sync telesrv 73f5c91 (feat(account): implement unified account deletion lifecycle). Skipped telesrv docs changes per public sync rules.
This commit is contained in:
parent
96a419b565
commit
edb7057757
32 changed files with 3236 additions and 88 deletions
737
internal/store/postgres/account_lifecycle.go
Normal file
737
internal/store/postgres/account_lifecycle.go
Normal file
|
|
@ -0,0 +1,737 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// AccountLifecycleStore is the PostgreSQL implementation of the unified
|
||||
// account tombstone, delayed deletion and deletion notification boundary.
|
||||
type AccountLifecycleStore struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
func NewAccountLifecycleStore(pool *pgxpool.Pool) *AccountLifecycleStore {
|
||||
return &AccountLifecycleStore{pool: pool}
|
||||
}
|
||||
|
||||
func (s *AccountLifecycleStore) AccountDeletionSnapshot(ctx context.Context, userID int64) (domain.AccountDeletionSnapshot, bool, error) {
|
||||
if s == nil || s.pool == nil || userID == 0 {
|
||||
return domain.AccountDeletionSnapshot{}, false, nil
|
||||
}
|
||||
u, found, err := NewUserStore(s.pool).ByID(ctx, userID)
|
||||
if err != nil || !found {
|
||||
return domain.AccountDeletionSnapshot{}, found, err
|
||||
}
|
||||
var snapshot = domain.AccountDeletionSnapshot{User: u}
|
||||
if err := s.pool.QueryRow(ctx, `
|
||||
SELECT EXISTS (SELECT 1 FROM account_passwords WHERE user_id = $1 AND has_password),
|
||||
COALESCE((SELECT password_changed_at FROM account_passwords WHERE user_id = $1), u.created_at)
|
||||
FROM users u WHERE u.id = $1`, userID).Scan(&snapshot.HasPassword, &snapshot.PasswordUpdatedAt); err != nil {
|
||||
return domain.AccountDeletionSnapshot{}, false, fmt.Errorf("load account deletion password facts: %w", err)
|
||||
}
|
||||
pending, ok, err := pendingAccountDeletion(ctx, s.pool, userID, nil)
|
||||
if err != nil {
|
||||
return domain.AccountDeletionSnapshot{}, false, err
|
||||
}
|
||||
if ok {
|
||||
snapshot.Pending = &pending
|
||||
}
|
||||
return snapshot, true, nil
|
||||
}
|
||||
|
||||
func (s *AccountLifecycleStore) ScheduleAccountDeletion(ctx context.Context, req domain.ScheduleAccountDeletion) (domain.AccountDeletionRequest, bool, error) {
|
||||
if s == nil || s.pool == nil || req.UserID == 0 || req.RequestedAt.IsZero() || !req.ExecuteAt.After(req.RequestedAt) {
|
||||
return domain.AccountDeletionRequest{}, false, domain.ErrAccountDeletionForbidden
|
||||
}
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return domain.AccountDeletionRequest{}, false, fmt.Errorf("begin schedule account deletion: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if err := lockUsersForUpdate(ctx, tx, req.UserID, domain.OfficialSystemUserID); err != nil {
|
||||
return domain.AccountDeletionRequest{}, false, fmt.Errorf("lock schedule account deletion users: %w", err)
|
||||
}
|
||||
var deletedAt *time.Time
|
||||
if err := tx.QueryRow(ctx, `SELECT deleted_at FROM users WHERE id = $1 FOR UPDATE`, req.UserID).Scan(&deletedAt); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.AccountDeletionRequest{}, false, domain.ErrUserNotFound
|
||||
}
|
||||
return domain.AccountDeletionRequest{}, false, fmt.Errorf("lock account deletion user: %w", err)
|
||||
}
|
||||
if deletedAt != nil {
|
||||
return domain.AccountDeletionRequest{}, false, domain.ErrAccountDeleted
|
||||
}
|
||||
if existing, ok, err := pendingAccountDeletion(ctx, tx, req.UserID, nil); err != nil {
|
||||
return domain.AccountDeletionRequest{}, false, err
|
||||
} else if ok {
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return domain.AccountDeletionRequest{}, false, fmt.Errorf("commit existing account deletion: %w", err)
|
||||
}
|
||||
return existing, false, nil
|
||||
}
|
||||
row := tx.QueryRow(ctx, `
|
||||
INSERT INTO account_deletion_requests (
|
||||
user_id, requester_auth_key_id, reason, confirm_hash_digest, requested_at, execute_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, user_id, requester_auth_key_id, state, reason, confirm_hash_digest,
|
||||
requested_at, execute_at, completed_at`,
|
||||
req.UserID, authKeyIDToInt64(req.RequesterAuthKeyID), req.Reason,
|
||||
req.ConfirmHashDigest[:], req.RequestedAt, req.ExecuteAt)
|
||||
pending, err := scanAccountDeletionRequest(row)
|
||||
if err != nil {
|
||||
return domain.AccountDeletionRequest{}, false, fmt.Errorf("insert account deletion request: %w", err)
|
||||
}
|
||||
randomID := int64(binary.LittleEndian.Uint64(req.ConfirmHashDigest[:8]))
|
||||
if randomID == 0 {
|
||||
randomID = pending.ID
|
||||
}
|
||||
if _, err := NewMessageStore(tx).SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: domain.OfficialSystemUserID,
|
||||
RecipientUserID: req.UserID,
|
||||
RandomID: randomID,
|
||||
Message: req.ServiceMessage,
|
||||
Date: int(req.RequestedAt.Unix()),
|
||||
}); err != nil {
|
||||
return domain.AccountDeletionRequest{}, false, fmt.Errorf("send account deletion confirmation message: %w", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return domain.AccountDeletionRequest{}, false, fmt.Errorf("commit schedule account deletion: %w", err)
|
||||
}
|
||||
return pending, true, nil
|
||||
}
|
||||
|
||||
func (s *AccountLifecycleStore) PendingAccountDeletionByHash(ctx context.Context, userID int64, digest [32]byte) (domain.AccountDeletionRequest, bool, error) {
|
||||
if s == nil || s.pool == nil || userID == 0 {
|
||||
return domain.AccountDeletionRequest{}, false, nil
|
||||
}
|
||||
return pendingAccountDeletion(ctx, s.pool, userID, digest[:])
|
||||
}
|
||||
|
||||
func (s *AccountLifecycleStore) ExecuteAccountDeletion(ctx context.Context, userID int64, source domain.AccountDeletionSource, reason string, now time.Time) (domain.AccountDeletionResult, error) {
|
||||
if s == nil || s.pool == nil || userID == 0 || now.IsZero() || !validAccountDeletionSource(source) {
|
||||
return domain.AccountDeletionResult{}, domain.ErrAccountDeletionForbidden
|
||||
}
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return domain.AccountDeletionResult{}, fmt.Errorf("begin execute account deletion: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if err := lockUsersForUpdate(ctx, tx, userID); err != nil {
|
||||
return domain.AccountDeletionResult{}, fmt.Errorf("lock account deletion user: %w", err)
|
||||
}
|
||||
var lockedID int64
|
||||
if err := tx.QueryRow(ctx, `SELECT id FROM users WHERE id = $1 FOR UPDATE`, userID).Scan(&lockedID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.AccountDeletionResult{}, domain.ErrUserNotFound
|
||||
}
|
||||
return domain.AccountDeletionResult{}, fmt.Errorf("lock account deletion row: %w", err)
|
||||
}
|
||||
u, found, err := NewUserStore(tx).ByID(ctx, userID)
|
||||
if err != nil {
|
||||
return domain.AccountDeletionResult{}, err
|
||||
}
|
||||
if !found {
|
||||
return domain.AccountDeletionResult{}, domain.ErrUserNotFound
|
||||
}
|
||||
if u.Deleted {
|
||||
return domain.AccountDeletionResult{User: u, Changed: false}, nil
|
||||
}
|
||||
if u.Bot || domain.IsSystemUserID(u.ID) {
|
||||
return domain.AccountDeletionResult{}, domain.ErrAccountDeletionForbidden
|
||||
}
|
||||
due, err := accountDeletionStillDue(ctx, tx, u, source, now)
|
||||
if err != nil {
|
||||
return domain.AccountDeletionResult{}, err
|
||||
}
|
||||
if !due {
|
||||
return domain.AccountDeletionResult{User: u, Changed: false}, nil
|
||||
}
|
||||
if err := enqueueAccountDeletionNotifications(ctx, tx, userID); err != nil {
|
||||
return domain.AccountDeletionResult{}, err
|
||||
}
|
||||
if err := settleDeletedAccountFinancialState(ctx, tx, userID, now); err != nil {
|
||||
return domain.AccountDeletionResult{}, err
|
||||
}
|
||||
revoked, err := revokeByUserExceptTx(ctx, tx, userID, 0)
|
||||
if err != nil {
|
||||
return domain.AccountDeletionResult{}, fmt.Errorf("revoke deleted account authorizations: %w", err)
|
||||
}
|
||||
if err := purgeDeletedAccountPrivateState(ctx, tx, userID, now); err != nil {
|
||||
return domain.AccountDeletionResult{}, err
|
||||
}
|
||||
if err := replacePeerUsernameTx(ctx, tx, peerUsernameTypeUser, userID, ""); err != nil {
|
||||
return domain.AccountDeletionResult{}, fmt.Errorf("release deleted account username: %w", err)
|
||||
}
|
||||
reason = strings.TrimSpace(reason)
|
||||
reason = truncateUTF8Bytes(reason, 1024)
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE users SET
|
||||
phone = '', first_name = '', last_name = '', username = '', country_code = '', about = '',
|
||||
verified = false, support = false, last_seen_at = 0,
|
||||
premium_expires_at = NULL, emoji_status_document_id = 0, emoji_status_until = 0,
|
||||
color_set = false, color = 0, color_background_emoji_id = 0,
|
||||
profile_color_set = false, profile_color = 0, profile_color_background_emoji_id = 0,
|
||||
birthday_day = 0, birthday_month = 0, birthday_year = 0, personal_channel_id = 0,
|
||||
deleted_at = $2, deletion_source = $3, deletion_reason = $4,
|
||||
account_delete_at = NULL, updated_at = $2
|
||||
WHERE id = $1 AND deleted_at IS NULL`, userID, now, string(source), reason); err != nil {
|
||||
return domain.AccountDeletionResult{}, fmt.Errorf("write deleted account tombstone: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE account_deletion_requests
|
||||
SET state = 'executed', completed_at = $2, updated_at = $2
|
||||
WHERE user_id = $1 AND state = 'pending'`, userID, now); err != nil {
|
||||
return domain.AccountDeletionResult{}, fmt.Errorf("complete account deletion request: %w", err)
|
||||
}
|
||||
u, found, err = NewUserStore(tx).ByID(ctx, userID)
|
||||
if err != nil || !found {
|
||||
if err == nil {
|
||||
err = domain.ErrUserNotFound
|
||||
}
|
||||
return domain.AccountDeletionResult{}, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return domain.AccountDeletionResult{}, fmt.Errorf("commit execute account deletion: %w", err)
|
||||
}
|
||||
return domain.AccountDeletionResult{User: u, Changed: true, RevokedAuthorizations: revoked}, nil
|
||||
}
|
||||
|
||||
func (s *AccountLifecycleStore) CancelAccountDeletion(ctx context.Context, userID int64, digest [32]byte, now time.Time) ([]domain.Authorization, error) {
|
||||
if s == nil || s.pool == nil || userID == 0 || now.IsZero() {
|
||||
return nil, domain.ErrAccountDeletionHashInvalid
|
||||
}
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin cancel account deletion: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if err := lockUsersForUpdate(ctx, tx, userID); err != nil {
|
||||
return nil, fmt.Errorf("lock cancel account deletion: %w", err)
|
||||
}
|
||||
pending, ok, err := pendingAccountDeletionForUpdate(ctx, tx, userID, digest[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, domain.ErrAccountDeletionHashInvalid
|
||||
}
|
||||
revoked, err := revokeOneAuthorizationTx(ctx, tx, userID, pending.RequesterAuthKeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE account_deletion_requests
|
||||
SET state = 'cancelled', completed_at = $2, updated_at = $2
|
||||
WHERE id = $1 AND state = 'pending'`, pending.ID, now); err != nil {
|
||||
return nil, fmt.Errorf("cancel account deletion request: %w", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, fmt.Errorf("commit cancel account deletion: %w", err)
|
||||
}
|
||||
return revoked, nil
|
||||
}
|
||||
|
||||
func (s *AccountLifecycleStore) DueAccountDeletions(ctx context.Context, now time.Time, limit int) ([]domain.AccountDeletionCandidate, error) {
|
||||
if s == nil || s.pool == nil || limit <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
WITH candidates AS (
|
||||
SELECT user_id, 'password_reset_expiry'::text AS source, execute_at AS due_at, 1 AS priority
|
||||
FROM account_deletion_requests WHERE state = 'pending' AND execute_at <= $1
|
||||
UNION ALL
|
||||
SELECT id, 'account_ttl', account_delete_at, 2
|
||||
FROM users WHERE deleted_at IS NULL AND is_bot = false AND account_delete_at <= $1
|
||||
UNION ALL
|
||||
SELECT r.user_id, 'freeze_expiry', r.frozen_until, 3
|
||||
FROM account_restrictions r JOIN users u ON u.id = r.user_id
|
||||
WHERE r.frozen = true AND r.frozen_until IS NOT NULL AND r.frozen_until <= $1
|
||||
AND u.deleted_at IS NULL AND u.is_bot = false
|
||||
), dedup AS (
|
||||
SELECT DISTINCT ON (user_id) user_id, source, due_at
|
||||
FROM candidates ORDER BY user_id, priority, due_at
|
||||
)
|
||||
SELECT user_id, source, due_at FROM dedup ORDER BY due_at, user_id LIMIT $2`, now, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list due account deletions: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]domain.AccountDeletionCandidate, 0)
|
||||
for rows.Next() {
|
||||
var c domain.AccountDeletionCandidate
|
||||
var source string
|
||||
if err := rows.Scan(&c.UserID, &source, &c.DueAt); err != nil {
|
||||
return nil, fmt.Errorf("scan due account deletion: %w", err)
|
||||
}
|
||||
c.Source = domain.AccountDeletionSource(source)
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *AccountLifecycleStore) ClaimAccountDeletionNotifications(ctx context.Context, now time.Time, limit int, lease time.Duration) ([]domain.AccountDeletionNotification, error) {
|
||||
if s == nil || s.pool == nil || limit <= 0 || lease <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
WITH claim AS (
|
||||
SELECT id FROM account_deletion_notifications
|
||||
WHERE (status = 'pending' AND next_attempt_at <= $1)
|
||||
OR (status = 'dispatching' AND lease_until <= $1)
|
||||
ORDER BY next_attempt_at, id FOR UPDATE SKIP LOCKED LIMIT $2
|
||||
)
|
||||
UPDATE account_deletion_notifications n
|
||||
SET status = 'dispatching', attempts = attempts + 1, lease_until = $3, updated_at = $1
|
||||
FROM claim WHERE n.id = claim.id
|
||||
RETURNING n.id, n.target_user_id, n.deleted_user_id, n.attempts`, now, limit, now.Add(lease))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("claim account deletion notifications: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]domain.AccountDeletionNotification, 0)
|
||||
for rows.Next() {
|
||||
var n domain.AccountDeletionNotification
|
||||
if err := rows.Scan(&n.ID, &n.TargetUserID, &n.DeletedUserID, &n.Attempts); err != nil {
|
||||
return nil, fmt.Errorf("scan account deletion notification: %w", err)
|
||||
}
|
||||
out = append(out, n)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *AccountLifecycleStore) CompleteAccountDeletionNotification(ctx context.Context, id int64, now time.Time) error {
|
||||
_, err := s.pool.Exec(ctx, `UPDATE account_deletion_notifications
|
||||
SET status = 'delivered', lease_until = NULL, last_error = '', updated_at = $2 WHERE id = $1`, id, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("complete account deletion notification: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type accountDeletionRowScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanAccountDeletionRequest(row accountDeletionRowScanner) (domain.AccountDeletionRequest, error) {
|
||||
var (
|
||||
r domain.AccountDeletionRequest
|
||||
authKey int64
|
||||
state string
|
||||
digest []byte
|
||||
completedAt *time.Time
|
||||
)
|
||||
if err := row.Scan(&r.ID, &r.UserID, &authKey, &state, &r.Reason, &digest,
|
||||
&r.RequestedAt, &r.ExecuteAt, &completedAt); err != nil {
|
||||
return domain.AccountDeletionRequest{}, err
|
||||
}
|
||||
if len(digest) != len(r.ConfirmHashDigest) {
|
||||
return domain.AccountDeletionRequest{}, fmt.Errorf("invalid account deletion digest length %d", len(digest))
|
||||
}
|
||||
copy(r.ConfirmHashDigest[:], digest)
|
||||
r.RequesterAuthKeyID = authKeyIDFromInt64(authKey)
|
||||
r.State = domain.AccountDeletionRequestState(state)
|
||||
if completedAt != nil {
|
||||
r.CompletedAt = *completedAt
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func pendingAccountDeletion(ctx context.Context, db interface {
|
||||
QueryRow(context.Context, string, ...any) pgx.Row
|
||||
}, userID int64, digest []byte) (domain.AccountDeletionRequest, bool, error) {
|
||||
query := `SELECT id, user_id, requester_auth_key_id, state, reason, confirm_hash_digest,
|
||||
requested_at, execute_at, completed_at FROM account_deletion_requests
|
||||
WHERE user_id = $1 AND state = 'pending'`
|
||||
args := []any{userID}
|
||||
if digest != nil {
|
||||
query += ` AND confirm_hash_digest = $2`
|
||||
args = append(args, digest)
|
||||
}
|
||||
r, err := scanAccountDeletionRequest(db.QueryRow(ctx, query, args...))
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.AccountDeletionRequest{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.AccountDeletionRequest{}, false, fmt.Errorf("load pending account deletion: %w", err)
|
||||
}
|
||||
return r, true, nil
|
||||
}
|
||||
|
||||
func pendingAccountDeletionForUpdate(ctx context.Context, tx pgx.Tx, userID int64, digest []byte) (domain.AccountDeletionRequest, bool, error) {
|
||||
row := tx.QueryRow(ctx, `SELECT id, user_id, requester_auth_key_id, state, reason, confirm_hash_digest,
|
||||
requested_at, execute_at, completed_at FROM account_deletion_requests
|
||||
WHERE user_id = $1 AND confirm_hash_digest = $2 AND state = 'pending' FOR UPDATE`, userID, digest)
|
||||
r, err := scanAccountDeletionRequest(row)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.AccountDeletionRequest{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return domain.AccountDeletionRequest{}, false, fmt.Errorf("lock pending account deletion: %w", err)
|
||||
}
|
||||
return r, true, nil
|
||||
}
|
||||
|
||||
func validAccountDeletionSource(source domain.AccountDeletionSource) bool {
|
||||
switch source {
|
||||
case domain.AccountDeletionManual, domain.AccountDeletionForgotPassword, domain.AccountDeletionTOSDecline,
|
||||
domain.AccountDeletionPasswordResetExpiry, domain.AccountDeletionAccountTTL, domain.AccountDeletionFreezeExpiry:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// accountDeletionStillDue closes the list-then-execute race for every scheduled
|
||||
// source. The user row is already locked; source-specific facts are read and,
|
||||
// where applicable, locked again immediately before destructive work begins.
|
||||
// Manual sources are admission decisions made by the caller and have no
|
||||
// independently mutable deadline to revalidate.
|
||||
func accountDeletionStillDue(ctx context.Context, tx pgx.Tx, user domain.User, source domain.AccountDeletionSource, now time.Time) (bool, error) {
|
||||
switch source {
|
||||
case domain.AccountDeletionManual, domain.AccountDeletionForgotPassword, domain.AccountDeletionTOSDecline:
|
||||
return true, nil
|
||||
case domain.AccountDeletionPasswordResetExpiry:
|
||||
var requestID int64
|
||||
err := tx.QueryRow(ctx, `
|
||||
SELECT id FROM account_deletion_requests
|
||||
WHERE user_id = $1 AND state = 'pending' AND execute_at <= $2
|
||||
ORDER BY execute_at, id LIMIT 1 FOR UPDATE`, user.ID, now).Scan(&requestID)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("revalidate pending account deletion: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
case domain.AccountDeletionAccountTTL:
|
||||
return !user.AccountDeleteAt.IsZero() && !user.AccountDeleteAt.After(now), nil
|
||||
case domain.AccountDeletionFreezeExpiry:
|
||||
var frozen bool
|
||||
var frozenUntil *time.Time
|
||||
err := tx.QueryRow(ctx, `
|
||||
SELECT frozen, frozen_until FROM account_restrictions WHERE user_id = $1 FOR UPDATE`, user.ID).Scan(&frozen, &frozenUntil)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("revalidate frozen account deletion: %w", err)
|
||||
}
|
||||
return frozen && frozenUntil != nil && !frozenUntil.After(now), nil
|
||||
default:
|
||||
return false, domain.ErrAccountDeletionForbidden
|
||||
}
|
||||
}
|
||||
|
||||
func truncateUTF8Bytes(value string, maxBytes int) string {
|
||||
if maxBytes < 1 {
|
||||
return ""
|
||||
}
|
||||
if len(value) <= maxBytes {
|
||||
return value
|
||||
}
|
||||
cut := maxBytes
|
||||
for cut > 0 && !utf8.ValidString(value[:cut]) {
|
||||
cut--
|
||||
}
|
||||
return value[:cut]
|
||||
}
|
||||
|
||||
func enqueueAccountDeletionNotifications(ctx context.Context, tx pgx.Tx, userID int64) error {
|
||||
const maxAccountDeletionNotificationAudience = 4096
|
||||
_, err := tx.Exec(ctx, `
|
||||
INSERT INTO account_deletion_notifications (target_user_id, deleted_user_id)
|
||||
SELECT audience.user_id, $1
|
||||
FROM (
|
||||
SELECT user_id
|
||||
FROM (
|
||||
SELECT contact_user_id AS user_id, 0 AS priority, 0 AS activity
|
||||
FROM contacts WHERE user_id = $1
|
||||
UNION ALL
|
||||
SELECT user_id, 0, 0 FROM contacts WHERE contact_user_id = $1
|
||||
UNION ALL
|
||||
SELECT peer_id, 1, top_message_date
|
||||
FROM dialogs WHERE user_id = $1 AND peer_type = 'user'
|
||||
UNION ALL
|
||||
SELECT user_id, 1, top_message_date
|
||||
FROM dialogs WHERE peer_type = 'user' AND peer_id = $1
|
||||
) candidates
|
||||
GROUP BY user_id
|
||||
ORDER BY min(priority), max(activity) DESC, user_id
|
||||
LIMIT $2
|
||||
) audience
|
||||
JOIN users u ON u.id = audience.user_id
|
||||
WHERE audience.user_id <> $1 AND u.deleted_at IS NULL
|
||||
ON CONFLICT (target_user_id, deleted_user_id) DO NOTHING`, userID, maxAccountDeletionNotificationAudience)
|
||||
if err != nil {
|
||||
return fmt.Errorf("enqueue account deletion notifications: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func revokeOneAuthorizationTx(ctx context.Context, tx pgx.Tx, userID int64, authKeyID [8]byte) ([]domain.Authorization, error) {
|
||||
id := authKeyIDToInt64(authKeyID)
|
||||
if id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err := lockPermanentAuthIdentities(ctx, tx, []int64{id}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var locked int64
|
||||
if err := tx.QueryRow(ctx, `SELECT auth_key_id FROM auth_keys WHERE auth_key_id = $1 FOR UPDATE`, id).Scan(&locked); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("lock password reset auth key: %w", err)
|
||||
}
|
||||
a, found, err := scanRevokedAuthorization(tx.QueryRow(ctx, `
|
||||
SELECT auth_key_id, user_id, hash, layer, device_model, platform, system_version,
|
||||
api_id, app_version, ip, password_pending, created_at, active_at
|
||||
FROM authorizations WHERE auth_key_id = $1 AND user_id = $2 FOR UPDATE`, id, userID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load password reset authorization: %w", err)
|
||||
}
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
if err := deleteRevocationTargetsTx(ctx, tx, []int64{id}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []domain.Authorization{a}, nil
|
||||
}
|
||||
|
||||
func purgeDeletedAccountPrivateState(ctx context.Context, tx pgx.Tx, userID int64, now time.Time) error {
|
||||
// Leave shared private_messages/channel_messages and immutable transaction
|
||||
// ledgers intact. Only the deleted user's private projections and settings are
|
||||
// removed; other users continue to reference the tombstone sender.
|
||||
statements := []string{
|
||||
`DELETE FROM account_privacy_rules WHERE owner_user_id = $1`,
|
||||
`DELETE FROM account_reaction_settings WHERE user_id = $1`,
|
||||
`DELETE FROM account_restrictions WHERE user_id = $1`,
|
||||
`DELETE FROM account_settings WHERE user_id = $1`,
|
||||
`DELETE FROM account_passwords WHERE user_id = $1`,
|
||||
`DELETE FROM notify_settings WHERE owner_user_id = $1`,
|
||||
`DELETE FROM passkey_credentials WHERE user_id = $1`,
|
||||
`DELETE FROM contacts WHERE user_id = $1 OR contact_user_id = $1`,
|
||||
`DELETE FROM contact_blocks WHERE owner_user_id = $1 OR blocked_user_id = $1`,
|
||||
`DELETE FROM dialog_drafts WHERE user_id = $1`,
|
||||
`DELETE FROM dialog_filter_settings WHERE user_id = $1`,
|
||||
`DELETE FROM dialog_filters WHERE user_id = $1`,
|
||||
`DELETE FROM chatlist_memberships WHERE user_id = $1 OR owner_user_id = $1`,
|
||||
`DELETE FROM chatlist_invites WHERE owner_user_id = $1`,
|
||||
`DELETE FROM saved_dialog_pins WHERE user_id = $1`,
|
||||
`DELETE FROM message_box_media WHERE owner_user_id = $1`,
|
||||
`DELETE FROM private_media_category_counts WHERE owner_user_id = $1`,
|
||||
`DELETE FROM message_boxes WHERE owner_user_id = $1`,
|
||||
`DELETE FROM dialogs WHERE user_id = $1`,
|
||||
`DELETE FROM dispatch_outbox WHERE target_user_id = $1`,
|
||||
`DELETE FROM dispatch_outbox_user_heads WHERE target_user_id = $1`,
|
||||
`DELETE FROM user_update_events WHERE user_id = $1`,
|
||||
`DELETE FROM user_update_retention WHERE user_id = $1`,
|
||||
`DELETE FROM user_update_watermarks WHERE user_id = $1`,
|
||||
`DELETE FROM update_states WHERE user_id = $1`,
|
||||
`DELETE FROM bootstrap_update_jobs WHERE user_id = $1`,
|
||||
`DELETE FROM scheduled_messages WHERE owner_user_id = $1`,
|
||||
`DELETE FROM quick_reply_messages WHERE owner_user_id = $1`,
|
||||
`DELETE FROM quick_replies WHERE owner_user_id = $1`,
|
||||
`DELETE FROM saved_music WHERE user_id = $1`,
|
||||
`DELETE FROM user_sticker_collections WHERE owner_user_id = $1`,
|
||||
`DELETE FROM user_sticker_sets WHERE owner_user_id = $1`,
|
||||
`DELETE FROM user_recent_reactions WHERE user_id = $1`,
|
||||
`DELETE FROM user_saved_reaction_tags WHERE user_id = $1`,
|
||||
`DELETE FROM user_top_reactions WHERE user_id = $1`,
|
||||
`DELETE FROM theme_user_installs WHERE user_id = $1`,
|
||||
`DELETE FROM peer_translation_settings WHERE user_id = $1`,
|
||||
`DELETE FROM ai_compose_tone_saves WHERE user_id = $1`,
|
||||
`DELETE FROM ai_compose_tones WHERE owner_user_id = $1`,
|
||||
`DELETE FROM business_automation_deliveries WHERE owner_user_id = $1 OR peer_user_id = $1`,
|
||||
`DELETE FROM business_connected_bot_peer_states WHERE owner_user_id = $1 OR peer_user_id = $1`,
|
||||
`DELETE FROM business_connected_bots WHERE owner_user_id = $1`,
|
||||
`DELETE FROM business_chat_links WHERE owner_user_id = $1`,
|
||||
`DELETE FROM user_business_profiles WHERE user_id = $1`,
|
||||
`DELETE FROM attach_menu_user_states WHERE user_id = $1`,
|
||||
`DELETE FROM bot_emoji_status_permissions WHERE user_id = $1`,
|
||||
`DELETE FROM bot_user_permissions WHERE user_id = $1`,
|
||||
`DELETE FROM login_code_message_deliveries WHERE user_id = $1`,
|
||||
`DELETE FROM webview_custom_method_queries WHERE user_id = $1`,
|
||||
`DELETE FROM webview_requested_buttons WHERE user_id = $1`,
|
||||
`DELETE FROM profile_photos WHERE owner_peer_type = 'user' AND owner_peer_id = $1`,
|
||||
`DELETE FROM story_views WHERE viewer_user_id = $1 OR (owner_peer_type = 'user' AND owner_peer_id = $1)`,
|
||||
`DELETE FROM story_exposures WHERE viewer_user_id = $1 OR (owner_peer_type = 'user' AND owner_peer_id = $1)`,
|
||||
`DELETE FROM story_read_states WHERE viewer_user_id = $1 OR (owner_peer_type = 'user' AND owner_peer_id = $1)`,
|
||||
`DELETE FROM story_hidden_peers WHERE viewer_user_id = $1 OR (owner_peer_type = 'user' AND owner_peer_id = $1)`,
|
||||
`DELETE FROM stories WHERE owner_peer_type = 'user' AND owner_peer_id = $1`,
|
||||
`DELETE FROM group_call_schedule_subscribers WHERE user_id = $1`,
|
||||
`DELETE FROM group_call_participants WHERE user_id = $1`,
|
||||
`DELETE FROM group_call_invites WHERE inviter_user_id = $1 OR invitee_user_id = $1`,
|
||||
`DELETE FROM channel_boost_slots WHERE user_id = $1`,
|
||||
`DELETE FROM channel_invite_importers WHERE user_id = $1`,
|
||||
`DELETE FROM channel_topic_read WHERE user_id = $1`,
|
||||
`DELETE FROM channel_unread_mentions WHERE user_id = $1`,
|
||||
`DELETE FROM channel_unread_mention_index WHERE user_id = $1`,
|
||||
`DELETE FROM channel_dialogs WHERE user_id = $1`,
|
||||
`DELETE FROM user_channel_member_index WHERE user_id = $1`,
|
||||
`DELETE FROM account_deletion_notifications WHERE target_user_id = $1`,
|
||||
`DELETE FROM uploaded_media_receipts WHERE owner_user_id = $1`,
|
||||
`DELETE FROM upload_parts WHERE owner_user_id = $1`,
|
||||
`DELETE FROM encrypted_files WHERE owner_user_id = $1`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := tx.Exec(ctx, statement, userID); err != nil {
|
||||
return fmt.Errorf("purge deleted account private state (%s): %w", statement, err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
WITH changed AS (
|
||||
UPDATE channel_members
|
||||
SET status = 'left', left_at = $2, unread_mark = false, updated_at = $3
|
||||
WHERE user_id = $1 AND status = 'active'
|
||||
RETURNING channel_id, role
|
||||
), counts AS (
|
||||
SELECT channel_id, count(*) AS participants,
|
||||
count(*) FILTER (WHERE role IN ('creator', 'admin')) AS admins
|
||||
FROM changed GROUP BY channel_id
|
||||
)
|
||||
UPDATE channels c
|
||||
SET participants_count = GREATEST(0, c.participants_count - counts.participants::int),
|
||||
admins_count = GREATEST(0, c.admins_count - counts.admins::int),
|
||||
updated_at = $3
|
||||
FROM counts WHERE c.id = counts.channel_id`, userID, int(now.Unix()), now); err != nil {
|
||||
return fmt.Errorf("leave deleted account channels: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE secret_chats SET state = 'discarded', history_deleted = true,
|
||||
g_a = ''::bytea, g_b = ''::bytea, key_fingerprint = 0
|
||||
WHERE admin_user_id = $1 OR participant_user_id = $1`, userID); err != nil {
|
||||
return fmt.Errorf("discard deleted account secret chats: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settleDeletedAccountFinancialState(ctx context.Context, tx pgx.Tx, userID int64, now time.Time) error {
|
||||
nowUnix := int(now.Unix())
|
||||
rows, err := tx.Query(ctx, `
|
||||
SELECT id, buyer_user_id, currency, amount
|
||||
FROM star_gift_offers
|
||||
WHERE owner_peer_type = 'user' AND owner_peer_id = $1 AND status = 'pending'
|
||||
ORDER BY id FOR UPDATE`, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lock deleted account gift offers: %w", err)
|
||||
}
|
||||
type offer struct {
|
||||
id, buyer, amount int64
|
||||
currency string
|
||||
}
|
||||
offers := make([]offer, 0)
|
||||
for rows.Next() {
|
||||
var o offer
|
||||
if err := rows.Scan(&o.id, &o.buyer, &o.currency, &o.amount); err != nil {
|
||||
rows.Close()
|
||||
return fmt.Errorf("scan deleted account gift offer: %w", err)
|
||||
}
|
||||
offers = append(offers, o)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
rows.Close()
|
||||
for _, o := range offers {
|
||||
var balance int64
|
||||
if o.currency == "XTR" {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO stars_balances (user_id, balance) VALUES ($1, $2)
|
||||
ON CONFLICT (user_id) DO UPDATE SET balance = stars_balances.balance + EXCLUDED.balance, updated_at = now()
|
||||
RETURNING balance`, o.buyer, o.amount).Scan(&balance); err != nil {
|
||||
return fmt.Errorf("refund deleted account stars offer: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO stars_transactions
|
||||
(user_id, peer_type, peer_id, amount, reason, title, description, date)
|
||||
VALUES ($1, 'user', $2, $3, 'gift_offer_refund_account_deleted', 'Gift offer refunded', '', $4)`, o.buyer, userID, o.amount, nowUnix); err != nil {
|
||||
return fmt.Errorf("record deleted account stars refund: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.QueryRow(ctx, `
|
||||
INSERT INTO ton_balances (user_id, balance_nanoton) VALUES ($1, $2)
|
||||
ON CONFLICT (user_id) DO UPDATE SET balance_nanoton = ton_balances.balance_nanoton + EXCLUDED.balance_nanoton, updated_at = now()
|
||||
RETURNING balance_nanoton`, o.buyer, o.amount).Scan(&balance); err != nil {
|
||||
return fmt.Errorf("refund deleted account TON offer: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO ton_transactions
|
||||
(user_id, amount_nanoton, reason, peer_type, peer_id, date)
|
||||
VALUES ($1, $2, 'gift_offer_refund_account_deleted', 'user', $3, $4)`, o.buyer, o.amount, userID, nowUnix); err != nil {
|
||||
return fmt.Errorf("record deleted account TON refund: %w", err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `UPDATE star_gift_offers
|
||||
SET status = 'cancelled', resolved_at = $2, balance_after = $3
|
||||
WHERE id = $1 AND status = 'pending'`, o.id, nowUnix, balance); err != nil {
|
||||
return fmt.Errorf("cancel deleted account gift offer: %w", err)
|
||||
}
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `UPDATE star_gift_offers
|
||||
SET status = 'cancelled', resolved_at = $2, balance_after = 0
|
||||
WHERE buyer_user_id = $1 AND status = 'pending'`, userID, nowUnix); err != nil {
|
||||
return fmt.Errorf("cancel deleted buyer gift offers: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `UPDATE star_gift_withdrawal_requests
|
||||
SET status = 'failed', completed_at = $2 WHERE owner_user_id = $1 AND status = 'pending'`, userID, nowUnix); err != nil {
|
||||
return fmt.Errorf("fail deleted account withdrawals: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `UPDATE star_gift_auction_bids SET active = false, version = version + 1
|
||||
WHERE bidder_user_id = $1 AND active = true`, userID); err != nil {
|
||||
return fmt.Errorf("deactivate deleted account auction bids: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `UPDATE unique_star_gifts
|
||||
SET burned = true, owner_name = '', updated_at = $2
|
||||
WHERE owner_peer_type = 'user' AND owner_peer_id = $1`, userID, now); err != nil {
|
||||
return fmt.Errorf("burn deleted account unique gifts: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `UPDATE peer_star_gifts
|
||||
SET lifecycle_status = 'burned', unsaved = true, pinned_order = 0
|
||||
WHERE owner_peer_type = 'user' AND owner_peer_id = $1 AND unique_gift_id IS NOT NULL`, userID); err != nil {
|
||||
return fmt.Errorf("burn deleted account saved gifts: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM peer_star_gifts
|
||||
WHERE owner_peer_type = 'user' AND owner_peer_id = $1 AND unique_gift_id IS NULL`, userID); err != nil {
|
||||
return fmt.Errorf("delete deleted account regular gifts: %w", err)
|
||||
}
|
||||
var stars int64
|
||||
if err := tx.QueryRow(ctx, `SELECT balance FROM stars_balances WHERE user_id = $1 FOR UPDATE`, userID).Scan(&stars); err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return fmt.Errorf("lock deleted account stars balance: %w", err)
|
||||
}
|
||||
if stars != 0 {
|
||||
if _, err := tx.Exec(ctx, `UPDATE stars_balances SET balance = 0, updated_at = $2 WHERE user_id = $1`, userID, now); err != nil {
|
||||
return fmt.Errorf("zero deleted account stars: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO stars_transactions
|
||||
(user_id, peer_type, peer_id, amount, reason, title, description, date)
|
||||
VALUES ($1, 'user', $1, $2, 'account_deleted', 'Account deleted', '', $3)`, userID, -stars, nowUnix); err != nil {
|
||||
return fmt.Errorf("record deleted account stars clearing: %w", err)
|
||||
}
|
||||
}
|
||||
var ton int64
|
||||
if err := tx.QueryRow(ctx, `SELECT balance_nanoton FROM ton_balances WHERE user_id = $1 FOR UPDATE`, userID).Scan(&ton); err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||
return fmt.Errorf("lock deleted account TON balance: %w", err)
|
||||
}
|
||||
if ton != 0 {
|
||||
if _, err := tx.Exec(ctx, `UPDATE ton_balances SET balance_nanoton = 0, updated_at = $2 WHERE user_id = $1`, userID, now); err != nil {
|
||||
return fmt.Errorf("zero deleted account TON: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO ton_transactions
|
||||
(user_id, amount_nanoton, reason, date) VALUES ($1, $2, 'account_deleted', $3)`, userID, -ton, nowUnix); err != nil {
|
||||
return fmt.Errorf("record deleted account TON clearing: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
289
internal/store/postgres/account_lifecycle_integration_test.go
Normal file
289
internal/store/postgres/account_lifecycle_integration_test.go
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
func TestAccountLifecycleScheduleCancelAndTombstonePostgres(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
nonce := time.Now().UnixNano()
|
||||
users := NewUserStore(pool)
|
||||
deleted := createTestUser(t, ctx, users, fmt.Sprintf("15571%d", nonce), "Delete", "Me")
|
||||
peer := createTestUser(t, ctx, users, fmt.Sprintf("15572%d", nonce), "Keep", "Peer")
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM stars_transactions WHERE user_id = ANY($1)`, []int64{deleted.ID, peer.ID})
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM ton_transactions WHERE user_id = ANY($1)`, []int64{deleted.ID, peer.ID})
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM stars_balances WHERE user_id = ANY($1)`, []int64{deleted.ID, peer.ID})
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM ton_balances WHERE user_id = ANY($1)`, []int64{deleted.ID, peer.ID})
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM account_deletion_notifications WHERE target_user_id = ANY($1) OR deleted_user_id = ANY($1)`, []int64{deleted.ID, peer.ID})
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM account_deletion_requests WHERE user_id = ANY($1)`, []int64{deleted.ID, peer.ID})
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM private_messages WHERE sender_user_id = ANY($1) OR recipient_user_id = ANY($1)`, []int64{deleted.ID, peer.ID})
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM users WHERE id = ANY($1)`, []int64{deleted.ID, peer.ID})
|
||||
})
|
||||
|
||||
authOne := saveLifecycleTestAuthorization(t, ctx, pool, deleted.ID, 1)
|
||||
authTwo := saveLifecycleTestAuthorization(t, ctx, pool, deleted.ID, 2)
|
||||
if _, err := pool.Exec(ctx, `INSERT INTO contacts
|
||||
(user_id, contact_user_id, contact_phone, contact_first_name, contact_last_name)
|
||||
VALUES ($1, $2, 'stale-phone', 'Stale', 'Alias')`, peer.ID, deleted.ID); err != nil {
|
||||
t.Fatalf("insert reverse contact: %v", err)
|
||||
}
|
||||
if _, err := NewMessageStore(pool).SendPrivateText(ctx, domain.SendPrivateTextRequest{
|
||||
SenderUserID: deleted.ID, RecipientUserID: peer.ID, RandomID: nonce, Message: "keep shared history",
|
||||
}); err != nil {
|
||||
t.Fatalf("send shared message: %v", err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `INSERT INTO account_settings (user_id, account_ttl_days) VALUES ($1, 30)`, deleted.ID); err != nil {
|
||||
t.Fatalf("insert account settings: %v", err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `INSERT INTO stars_balances (user_id, balance) VALUES ($1, 50)`, deleted.ID); err != nil {
|
||||
t.Fatalf("insert stars balance: %v", err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `INSERT INTO ton_balances (user_id, balance_nanoton) VALUES ($1, 100)`, deleted.ID); err != nil {
|
||||
t.Fatalf("insert TON balance: %v", err)
|
||||
}
|
||||
|
||||
lifecycle := NewAccountLifecycleStore(pool)
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
digestOne := sha256.Sum256([]byte("confirm-one"))
|
||||
pending, created, err := lifecycle.ScheduleAccountDeletion(ctx, domain.ScheduleAccountDeletion{
|
||||
UserID: deleted.ID, RequesterAuthKeyID: authOne, Reason: "Forgot password",
|
||||
ConfirmHashDigest: digestOne, ServiceMessage: "tg://confirmphone?phone=hidden&hash=confirm-one",
|
||||
RequestedAt: now, ExecuteAt: now.Add(7 * 24 * time.Hour),
|
||||
})
|
||||
if err != nil || !created || pending.UserID != deleted.ID {
|
||||
t.Fatalf("schedule deletion = %+v created=%v err=%v", pending, created, err)
|
||||
}
|
||||
if got, found, err := lifecycle.PendingAccountDeletionByHash(ctx, deleted.ID, digestOne); err != nil || !found || got.ID != pending.ID {
|
||||
t.Fatalf("pending deletion by hash = %+v found=%v err=%v", got, found, err)
|
||||
}
|
||||
revoked, err := lifecycle.CancelAccountDeletion(ctx, deleted.ID, digestOne, now.Add(time.Minute))
|
||||
if err != nil || len(revoked) != 1 || revoked[0].AuthKeyID != authOne {
|
||||
t.Fatalf("cancel deletion revoked=%+v err=%v", revoked, err)
|
||||
}
|
||||
if _, found, err := NewAuthKeyStore(pool).Get(ctx, authOne); err != nil || found {
|
||||
t.Fatalf("requester auth key after cancel found=%v err=%v, want revoked", found, err)
|
||||
}
|
||||
if _, found, err := NewAuthKeyStore(pool).Get(ctx, authTwo); err != nil || !found {
|
||||
t.Fatalf("other auth key after cancel found=%v err=%v, want retained", found, err)
|
||||
}
|
||||
|
||||
result, err := lifecycle.ExecuteAccountDeletion(ctx, deleted.ID, domain.AccountDeletionManual, "manual", now.Add(2*time.Minute))
|
||||
if err != nil {
|
||||
t.Fatalf("execute account deletion: %v", err)
|
||||
}
|
||||
if !result.Changed || !result.User.Deleted || result.User.Phone != "" || result.User.FirstName != "" || len(result.RevokedAuthorizations) != 1 {
|
||||
t.Fatalf("deletion result = %+v", result)
|
||||
}
|
||||
if _, found, err := users.ByPhone(ctx, deleted.Phone); err != nil || found {
|
||||
t.Fatalf("released phone found=%v err=%v", found, err)
|
||||
}
|
||||
if tombstone, found, err := users.ByID(ctx, deleted.ID); err != nil || !found || !tombstone.Deleted || tombstone.FirstName != "" {
|
||||
t.Fatalf("tombstone = %+v found=%v err=%v", tombstone, found, err)
|
||||
}
|
||||
if _, err := users.UpdateProfile(ctx, deleted.ID, "Resurrected", "", ""); err == nil {
|
||||
t.Fatal("deleted account profile mutation unexpectedly succeeded")
|
||||
}
|
||||
history, err := NewMessageStore(pool).ListByUser(ctx, peer.ID, domain.MessageFilter{
|
||||
HasPeer: true,
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: deleted.ID},
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil || len(history.Messages) != 1 || history.Messages[0].Body != "keep shared history" || history.Messages[0].From.ID != deleted.ID {
|
||||
t.Fatalf("peer history after deletion = %+v err=%v", history, err)
|
||||
}
|
||||
var peerBoxes, settings, contacts, notifications int
|
||||
if err := pool.QueryRow(ctx, `SELECT count(*) FROM message_boxes WHERE owner_user_id = $1 AND from_user_id = $2`, peer.ID, deleted.ID).Scan(&peerBoxes); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT count(*) FROM account_settings WHERE user_id = $1`, deleted.ID).Scan(&settings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT count(*) FROM contacts WHERE user_id = $1 OR contact_user_id = $1`, deleted.ID).Scan(&contacts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT count(*) FROM account_deletion_notifications WHERE target_user_id = $1 AND deleted_user_id = $2`, peer.ID, deleted.ID).Scan(¬ifications); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if peerBoxes != 1 || settings != 0 || contacts != 0 || notifications != 1 {
|
||||
t.Fatalf("post-delete state peerBoxes=%d settings=%d contacts=%d notifications=%d", peerBoxes, settings, contacts, notifications)
|
||||
}
|
||||
var stars, ton, starClear, tonClear int64
|
||||
if err := pool.QueryRow(ctx, `SELECT balance FROM stars_balances WHERE user_id = $1`, deleted.ID).Scan(&stars); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT balance_nanoton FROM ton_balances WHERE user_id = $1`, deleted.ID).Scan(&ton); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT COALESCE(sum(amount), 0) FROM stars_transactions WHERE user_id = $1 AND reason = 'account_deleted'`, deleted.ID).Scan(&starClear); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT COALESCE(sum(amount_nanoton), 0) FROM ton_transactions WHERE user_id = $1 AND reason = 'account_deleted'`, deleted.ID).Scan(&tonClear); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stars != 0 || ton != 0 || starClear != -50 || tonClear != -100 {
|
||||
t.Fatalf("financial clearing stars=%d ton=%d star_tx=%d ton_tx=%d", stars, ton, starClear, tonClear)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountLifecycleDueSourcesAndTTLWatermarkPostgres(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
nonce := time.Now().UnixNano()
|
||||
users := NewUserStore(pool)
|
||||
ttlUser := createTestUser(t, ctx, users, fmt.Sprintf("15671%d", nonce), "TTL", "User")
|
||||
freezeUser := createTestUser(t, ctx, users, fmt.Sprintf("15672%d", nonce), "Frozen", "User")
|
||||
pendingUser := createTestUser(t, ctx, users, fmt.Sprintf("15673%d", nonce), "Pending", "User")
|
||||
ids := []int64{ttlUser.ID, freezeUser.ID, pendingUser.ID}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM account_deletion_notifications WHERE target_user_id = ANY($1) OR deleted_user_id = ANY($1)`, ids)
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM account_deletion_requests WHERE user_id = ANY($1)`, ids)
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM users WHERE id = ANY($1)`, ids)
|
||||
})
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
if _, err := pool.Exec(ctx, `UPDATE users SET account_delete_at = $2 WHERE id = $1`, ttlUser.ID, now.Add(-time.Minute)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `INSERT INTO account_restrictions
|
||||
(user_id, frozen, reason, actor, command_id, frozen_since, frozen_until, appeal_url)
|
||||
VALUES ($1, true, 'abuse', 'test', 'freeze-test', $2, $3, 'https://example.test/appeal')`, freezeUser.ID, now.Add(-time.Hour), now.Add(-time.Minute)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256([]byte("due-pending"))
|
||||
if _, err := pool.Exec(ctx, `INSERT INTO account_deletion_requests
|
||||
(user_id, requester_auth_key_id, reason, confirm_hash_digest, requested_at, execute_at)
|
||||
VALUES ($1, 123, 'forgot', $2, $3, $4)`, pendingUser.ID, digest[:], now.Add(-8*24*time.Hour), now.Add(-time.Minute)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
candidates, err := NewAccountLifecycleStore(pool).DueAccountDeletions(ctx, now, 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sources := make(map[int64]domain.AccountDeletionSource, len(candidates))
|
||||
for _, candidate := range candidates {
|
||||
sources[candidate.UserID] = candidate.Source
|
||||
}
|
||||
if sources[ttlUser.ID] != domain.AccountDeletionAccountTTL || sources[freezeUser.ID] != domain.AccountDeletionFreezeExpiry || sources[pendingUser.ID] != domain.AccountDeletionPasswordResetExpiry {
|
||||
t.Fatalf("due sources = %+v", sources)
|
||||
}
|
||||
seen := now.Add(time.Hour)
|
||||
if err := users.UpdateLastSeen(ctx, ttlUser.ID, int(seen.Unix())); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lifecycle := NewAccountLifecycleStore(pool)
|
||||
if stale, err := lifecycle.ExecuteAccountDeletion(ctx, ttlUser.ID, domain.AccountDeletionAccountTTL, "", now); err != nil || stale.Changed {
|
||||
t.Fatalf("stale TTL candidate changed=%v err=%v", stale.Changed, err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `UPDATE account_restrictions SET frozen_until = $2, updated_at = $3 WHERE user_id = $1`, freezeUser.ID, now.Add(24*time.Hour), now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stale, err := lifecycle.ExecuteAccountDeletion(ctx, freezeUser.ID, domain.AccountDeletionFreezeExpiry, "", now); err != nil || stale.Changed {
|
||||
t.Fatalf("extended freeze candidate changed=%v err=%v", stale.Changed, err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `UPDATE account_deletion_requests SET state = 'cancelled', completed_at = $2, updated_at = $2 WHERE user_id = $1`, pendingUser.ID, now); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stale, err := lifecycle.ExecuteAccountDeletion(ctx, pendingUser.ID, domain.AccountDeletionPasswordResetExpiry, "", now); err != nil || stale.Changed {
|
||||
t.Fatalf("cancelled pending candidate changed=%v err=%v", stale.Changed, err)
|
||||
}
|
||||
var deadline time.Time
|
||||
if err := pool.QueryRow(ctx, `SELECT account_delete_at FROM users WHERE id = $1`, ttlUser.ID).Scan(&deadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := seen.Add(365 * 24 * time.Hour); deadline.Sub(want) > time.Second || want.Sub(deadline) > time.Second {
|
||||
t.Fatalf("TTL watermark deadline=%v want=%v", deadline, want)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `INSERT INTO account_settings (user_id, account_ttl_days) VALUES ($1, 30)
|
||||
ON CONFLICT (user_id) DO UPDATE SET account_ttl_days = EXCLUDED.account_ttl_days`, ttlUser.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT account_delete_at FROM users WHERE id = $1`, ttlUser.ID).Scan(&deadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := seen.Add(30 * 24 * time.Hour); deadline.Sub(want) > time.Second || want.Sub(deadline) > time.Second {
|
||||
t.Fatalf("custom TTL deadline=%v want=%v", deadline, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountPasswordChangedAtIgnoresSRPChallengeRotationPostgres(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
user := createTestUser(t, ctx, NewUserStore(pool), fmt.Sprintf("15771%d", time.Now().UnixNano()), "Password", "Clock")
|
||||
t.Cleanup(func() { _, _ = pool.Exec(ctx, `DELETE FROM users WHERE id = $1`, user.ID) })
|
||||
if _, err := pool.Exec(ctx, `INSERT INTO account_passwords
|
||||
(user_id, has_password, current_algo_salt1, current_algo_salt2, current_algo_g, current_algo_p, srp_verifier, srp_id, srp_b)
|
||||
VALUES ($1, true, '\x01', '\x02', 3, '\x03', '\x04', 10, '\x05')`, user.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var initial, afterChallenge, afterPassword time.Time
|
||||
if err := pool.QueryRow(ctx, `SELECT password_changed_at FROM account_passwords WHERE user_id = $1`, user.ID).Scan(&initial); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `SELECT pg_sleep(0.02)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `UPDATE account_passwords SET srp_id = 11, srp_b = '\x06', updated_at = now() WHERE user_id = $1`, user.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT password_changed_at FROM account_passwords WHERE user_id = $1`, user.ID).Scan(&afterChallenge); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !afterChallenge.Equal(initial) {
|
||||
t.Fatalf("SRP challenge rotation changed password clock: initial=%v after=%v", initial, afterChallenge)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `SELECT pg_sleep(0.02)`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `UPDATE account_passwords SET srp_verifier = '\x07', updated_at = now() WHERE user_id = $1`, user.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT password_changed_at FROM account_passwords WHERE user_id = $1`, user.ID).Scan(&afterPassword); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !afterPassword.After(afterChallenge) {
|
||||
t.Fatalf("password verifier change did not advance clock: before=%v after=%v", afterChallenge, afterPassword)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateAccountDeletionReasonUTF8(t *testing.T) {
|
||||
got := truncateUTF8Bytes(strings.Repeat("界", 400), 1024)
|
||||
if !utf8.ValidString(got) || len(got) > 1024 {
|
||||
t.Fatalf("truncateUTF8Bytes returned invalid result: valid=%v bytes=%d", utf8.ValidString(got), len(got))
|
||||
}
|
||||
if got == "" {
|
||||
t.Fatal("truncateUTF8Bytes unexpectedly removed the whole reason")
|
||||
}
|
||||
}
|
||||
|
||||
func saveLifecycleTestAuthorization(t *testing.T, ctx context.Context, db *pgxpool.Pool, userID int64, marker byte) [8]byte {
|
||||
t.Helper()
|
||||
var id [8]byte
|
||||
var value [256]byte
|
||||
if _, err := rand.Read(id[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id[0] = marker
|
||||
if _, err := rand.Read(value[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := NewAuthKeyStore(db).Save(ctx, store.AuthKeyData{ID: id, Value: value}); err != nil {
|
||||
t.Fatalf("save lifecycle auth key: %v", err)
|
||||
}
|
||||
if err := NewAuthorizationStore(db).Bind(ctx, domain.Authorization{AuthKeyID: id, UserID: userID, Hash: int64(marker)}); err != nil {
|
||||
t.Fatalf("bind lifecycle authorization: %v", err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ func (s *MessageStore) DefaultHistoryTTL(ctx context.Context, userID int64) (int
|
|||
return 0, nil
|
||||
}
|
||||
var period int
|
||||
err := s.db.QueryRow(ctx, `SELECT COALESCE(default_history_ttl_period, 0)::int FROM users WHERE id = $1`, userID).Scan(&period)
|
||||
err := s.db.QueryRow(ctx, `SELECT COALESCE(default_history_ttl_period, 0)::int FROM users WHERE id = $1 AND deleted_at IS NULL`, userID).Scan(&period)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, nil
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ func (s *MessageStore) SetDefaultHistoryTTL(ctx context.Context, userID int64, p
|
|||
UPDATE users
|
||||
SET default_history_ttl_period = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
`, userID, period)
|
||||
if err != nil {
|
||||
return fmt.Errorf("set default history ttl: %w", err)
|
||||
|
|
|
|||
|
|
@ -8,16 +8,16 @@ WHERE id = ANY(sqlc.arg(ids)::bigint[])
|
|||
ORDER BY id;
|
||||
|
||||
-- name: GetUserByPhone :one
|
||||
SELECT * FROM users WHERE phone = $1;
|
||||
SELECT * FROM users WHERE phone = $1 AND deleted_at IS NULL;
|
||||
|
||||
-- name: GetUsersByPhones :many
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE phone = ANY(sqlc.arg(phones)::text[])
|
||||
WHERE phone = ANY(sqlc.arg(phones)::text[]) AND deleted_at IS NULL
|
||||
ORDER BY id;
|
||||
|
||||
-- name: GetUserByUsername :one
|
||||
SELECT * FROM users WHERE lower(username) = lower($1) AND username <> '';
|
||||
SELECT * FROM users WHERE lower(username) = lower($1) AND username <> '' AND deleted_at IS NULL;
|
||||
|
||||
-- name: SearchUsers :many
|
||||
WITH matched AS (
|
||||
|
|
@ -57,6 +57,7 @@ WITH matched AS (
|
|||
FROM users u
|
||||
LEFT JOIN contacts c ON c.user_id = sqlc.arg(current_user_id)::bigint AND c.contact_user_id = u.id
|
||||
WHERE u.id <> sqlc.arg(current_user_id)::bigint
|
||||
AND u.deleted_at IS NULL
|
||||
AND sqlc.arg(query_lower)::text <> ''
|
||||
AND (
|
||||
(sqlc.arg(phone_query)::text <> '' AND u.phone LIKE sqlc.arg(phone_query)::text || '%')
|
||||
|
|
@ -107,14 +108,14 @@ RETURNING *;
|
|||
UPDATE users
|
||||
SET username = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserLastSeen :exec
|
||||
UPDATE users
|
||||
SET last_seen_at = GREATEST(last_seen_at, sqlc.arg(last_seen_at)::bigint),
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint;
|
||||
WHERE id = sqlc.arg(id)::bigint AND deleted_at IS NULL;
|
||||
|
||||
-- name: UpdateUserProfile :one
|
||||
UPDATE users
|
||||
|
|
@ -122,28 +123,28 @@ SET first_name = $2,
|
|||
last_name = $3,
|
||||
about = $4,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserPhone :one
|
||||
UPDATE users
|
||||
SET phone = sqlc.arg(phone)::text,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint
|
||||
WHERE id = sqlc.arg(id)::bigint AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: SetUserPremiumUntil :one
|
||||
UPDATE users
|
||||
SET premium_expires_at = sqlc.narg(premium_expires_at)::timestamptz,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint
|
||||
WHERE id = sqlc.arg(id)::bigint AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: SetUserVerified :one
|
||||
UPDATE users
|
||||
SET verified = sqlc.arg(verified)::boolean,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint
|
||||
WHERE id = sqlc.arg(id)::bigint AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: SweepExpiredPremium :many
|
||||
|
|
@ -153,6 +154,7 @@ SET premium_expires_at = NULL,
|
|||
WHERE id IN (
|
||||
SELECT id FROM users
|
||||
WHERE premium_expires_at IS NOT NULL
|
||||
AND deleted_at IS NULL
|
||||
AND premium_expires_at <= sqlc.arg(now)::timestamptz
|
||||
ORDER BY premium_expires_at
|
||||
LIMIT sqlc.arg(limit_count)::int
|
||||
|
|
@ -164,7 +166,7 @@ UPDATE users
|
|||
SET emoji_status_document_id = sqlc.arg(emoji_status_document_id)::bigint,
|
||||
emoji_status_until = sqlc.arg(emoji_status_until)::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint
|
||||
WHERE id = sqlc.arg(id)::bigint AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserBirthday :one
|
||||
|
|
@ -173,14 +175,14 @@ SET birthday_day = sqlc.arg(birthday_day)::int,
|
|||
birthday_month = sqlc.arg(birthday_month)::int,
|
||||
birthday_year = sqlc.arg(birthday_year)::int,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint
|
||||
WHERE id = sqlc.arg(id)::bigint AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserPersonalChannel :one
|
||||
UPDATE users
|
||||
SET personal_channel_id = sqlc.arg(personal_channel_id)::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint
|
||||
WHERE id = sqlc.arg(id)::bigint AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserColor :one
|
||||
|
|
@ -189,7 +191,7 @@ SET color_set = sqlc.arg(color_set)::boolean,
|
|||
color = sqlc.arg(color)::int,
|
||||
color_background_emoji_id = sqlc.arg(background_emoji_id)::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint
|
||||
WHERE id = sqlc.arg(id)::bigint AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateUserProfileColor :one
|
||||
|
|
@ -198,5 +200,5 @@ SET profile_color_set = sqlc.arg(color_set)::boolean,
|
|||
profile_color = sqlc.arg(color)::int,
|
||||
profile_color_background_emoji_id = sqlc.arg(background_emoji_id)::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id)::bigint
|
||||
WHERE id = sqlc.arg(id)::bigint AND deleted_at IS NULL
|
||||
RETURNING *;
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ func (q *Queries) InsertBot(ctx context.Context, arg InsertBotParams) error {
|
|||
const insertBotUser = `-- name: InsertBotUser :one
|
||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, is_bot, bot_info_version)
|
||||
VALUES ($1, '', $2, '', $3, '', TRUE, 1)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type InsertBotUserParams struct {
|
||||
|
|
@ -209,6 +209,10 @@ func (q *Queries) InsertBotUser(ctx context.Context, arg InsertBotUserParams) (U
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,32 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type AccountDeletionNotification struct {
|
||||
ID int64
|
||||
TargetUserID int64
|
||||
DeletedUserID int64
|
||||
Status string
|
||||
Attempts int32
|
||||
NextAttemptAt pgtype.Timestamptz
|
||||
LeaseUntil pgtype.Timestamptz
|
||||
LastError string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type AccountDeletionRequest struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
RequesterAuthKeyID int64
|
||||
State string
|
||||
Reason string
|
||||
ConfirmHashDigest []byte
|
||||
RequestedAt pgtype.Timestamptz
|
||||
ExecuteAt pgtype.Timestamptz
|
||||
CompletedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type AccountPassword struct {
|
||||
UserID int64
|
||||
HasRecovery bool
|
||||
|
|
@ -30,6 +56,7 @@ type AccountPassword struct {
|
|||
RecoveryCode string
|
||||
RecoveryCodeExpiresAt pgtype.Timestamptz
|
||||
LoginEmail string
|
||||
PasswordChangedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type AccountPrivacyRule struct {
|
||||
|
|
@ -694,6 +721,42 @@ type ChannelMessageViewer struct {
|
|||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ChannelStarsBalance struct {
|
||||
ChannelID int64
|
||||
Balance int64
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ChannelStarsTransaction struct {
|
||||
ID int64
|
||||
ChannelID int64
|
||||
ActorUserID int64
|
||||
Amount int64
|
||||
Reason string
|
||||
PeerType string
|
||||
PeerID int64
|
||||
GiftID *int64
|
||||
Date int32
|
||||
}
|
||||
|
||||
type ChannelTonBalance struct {
|
||||
ChannelID int64
|
||||
BalanceNanoton int64
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ChannelTonTransaction struct {
|
||||
ID int64
|
||||
ChannelID int64
|
||||
ActorUserID int64
|
||||
AmountNanoton int64
|
||||
Reason string
|
||||
PeerType string
|
||||
PeerID int64
|
||||
GiftID *int64
|
||||
Date int32
|
||||
}
|
||||
|
||||
type ChannelTopicRead struct {
|
||||
ChannelID int64
|
||||
UserID int64
|
||||
|
|
@ -1156,19 +1219,33 @@ type PasskeyCredential struct {
|
|||
}
|
||||
|
||||
type PeerStarGift struct {
|
||||
ID int64
|
||||
OwnerPeerID int64
|
||||
FromUserID int64
|
||||
GiftID int64
|
||||
MsgID int32
|
||||
GiftDate int32
|
||||
NameHidden bool
|
||||
Unsaved bool
|
||||
Converted bool
|
||||
ConvertStars int64
|
||||
Message string
|
||||
OwnerPeerType string
|
||||
SavedID int64
|
||||
ID int64
|
||||
OwnerPeerID int64
|
||||
FromUserID int64
|
||||
GiftID int64
|
||||
MsgID int32
|
||||
GiftDate int32
|
||||
NameHidden bool
|
||||
Unsaved bool
|
||||
Converted bool
|
||||
ConvertStars int64
|
||||
Message string
|
||||
OwnerPeerType string
|
||||
SavedID int64
|
||||
CatalogRevisionID int64
|
||||
UniqueGiftID *int64
|
||||
UpgradeMsgID int32
|
||||
PinnedOrder int32
|
||||
PrepaidUpgradeStars int64
|
||||
LifecycleStatus string
|
||||
TransferStars int64
|
||||
PrepaidUpgradeHash string
|
||||
GiftNum int32
|
||||
CanExportAt int32
|
||||
CanTransferAt int32
|
||||
CanResellAt int32
|
||||
DropOriginalDetailsStars int64
|
||||
CanCraftAt int32
|
||||
}
|
||||
|
||||
type PeerTranslationSetting struct {
|
||||
|
|
@ -1417,6 +1494,382 @@ type SeedState struct {
|
|||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftAuction struct {
|
||||
GiftID int64
|
||||
Slug string
|
||||
Version int32
|
||||
StartDate int32
|
||||
EndDate int32
|
||||
RoundDuration int32
|
||||
GiftsPerRound int32
|
||||
TotalRounds int32
|
||||
CurrentRound int32
|
||||
NextRoundAt int32
|
||||
LastGiftNum int32
|
||||
GiftsLeft int32
|
||||
MinBidAmount int64
|
||||
Status string
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftAuctionAcquired struct {
|
||||
ID int64
|
||||
GiftID int64
|
||||
BidderUserID int64
|
||||
RecipientPeerType string
|
||||
RecipientPeerID int64
|
||||
SavedGiftID *int64
|
||||
BidAmount int64
|
||||
Round int32
|
||||
Pos int32
|
||||
GiftNum *int32
|
||||
AcquiredAt int32
|
||||
HideName bool
|
||||
Message string
|
||||
}
|
||||
|
||||
type StarGiftAuctionBid struct {
|
||||
GiftID int64
|
||||
BidderUserID int64
|
||||
RecipientPeerType string
|
||||
RecipientPeerID int64
|
||||
Amount int64
|
||||
BidDate int32
|
||||
HideName bool
|
||||
Message string
|
||||
Returned bool
|
||||
AcquiredCount int32
|
||||
Active bool
|
||||
Version int64
|
||||
}
|
||||
|
||||
type StarGiftAuctionBidPayment struct {
|
||||
UserID int64
|
||||
FormID int64
|
||||
GiftID int64
|
||||
BidAmount int64
|
||||
BalanceAfter int64
|
||||
CreatedAt int32
|
||||
}
|
||||
|
||||
type StarGiftCatalog struct {
|
||||
GiftID int64
|
||||
ActiveRevisionID int64
|
||||
Enabled bool
|
||||
SortOrder int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
CollectibleRevisionID *int64
|
||||
AvailabilityRemains int32
|
||||
AvailabilityResale int64
|
||||
ResellMinStars int64
|
||||
FirstSaleDate int32
|
||||
LastSaleDate int32
|
||||
}
|
||||
|
||||
type StarGiftCatalogRevision struct {
|
||||
ID int64
|
||||
GiftID int64
|
||||
Revision int32
|
||||
Title string
|
||||
Stars int64
|
||||
ConvertStars int64
|
||||
DocumentID int64
|
||||
AnimationJson []byte
|
||||
AnimationSha256 []byte
|
||||
SourceName string
|
||||
SourceFormat string
|
||||
Width int32
|
||||
Height int32
|
||||
FrameRate float64
|
||||
InPoint float64
|
||||
OutPoint float64
|
||||
CreatedBy string
|
||||
CommandID string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
OfficialGiftID *int64
|
||||
SourceManifestSha256 []byte
|
||||
OfficialSource []byte
|
||||
Limited bool
|
||||
SoldOut bool
|
||||
Birthday bool
|
||||
RequirePremium bool
|
||||
LimitedPerUser bool
|
||||
PeerColorAvailable bool
|
||||
Auction bool
|
||||
AvailabilityTotal int32
|
||||
ReleasedByPeerType *string
|
||||
ReleasedByPeerID *int64
|
||||
PerUserTotal int32
|
||||
LockedUntilDate int32
|
||||
AuctionSlug string
|
||||
GiftsPerRound int32
|
||||
AuctionStartDate int32
|
||||
UpgradeVariants int32
|
||||
BackgroundCenterColor *int32
|
||||
BackgroundEdgeColor *int32
|
||||
BackgroundTextColor *int32
|
||||
}
|
||||
|
||||
type StarGiftCollectibleBackdrop struct {
|
||||
ID int64
|
||||
CollectibleRevisionID int64
|
||||
Name string
|
||||
BackdropID int32
|
||||
CenterColor int32
|
||||
EdgeColor int32
|
||||
PatternColor int32
|
||||
TextColor int32
|
||||
RarityPermille *int32
|
||||
SortOrder int32
|
||||
RarityKind string
|
||||
}
|
||||
|
||||
type StarGiftCollectibleModel struct {
|
||||
ID int64
|
||||
CollectibleRevisionID int64
|
||||
Name string
|
||||
DocumentID int64
|
||||
AnimationJson []byte
|
||||
AnimationSha256 []byte
|
||||
SourceName string
|
||||
SourceFormat string
|
||||
Width int32
|
||||
Height int32
|
||||
FrameRate float64
|
||||
InPoint float64
|
||||
OutPoint float64
|
||||
RarityPermille *int32
|
||||
SortOrder int32
|
||||
RarityKind string
|
||||
Crafted bool
|
||||
OfficialDocumentID *int64
|
||||
}
|
||||
|
||||
type StarGiftCollectiblePattern struct {
|
||||
ID int64
|
||||
CollectibleRevisionID int64
|
||||
Name string
|
||||
DocumentID int64
|
||||
AnimationJson []byte
|
||||
AnimationSha256 []byte
|
||||
SourceName string
|
||||
SourceFormat string
|
||||
Width int32
|
||||
Height int32
|
||||
FrameRate float64
|
||||
InPoint float64
|
||||
OutPoint float64
|
||||
RarityPermille *int32
|
||||
SortOrder int32
|
||||
RarityKind string
|
||||
OfficialDocumentID *int64
|
||||
}
|
||||
|
||||
type StarGiftCollectibleRevision struct {
|
||||
ID int64
|
||||
GiftID int64
|
||||
Revision int32
|
||||
UpgradeStars int64
|
||||
SupplyTotal int32
|
||||
Issued int32
|
||||
SlugPrefix string
|
||||
Status string
|
||||
CreatedBy string
|
||||
CommandID string
|
||||
CreatedAt pgtype.Timestamptz
|
||||
PublishedAt pgtype.Timestamptz
|
||||
OfficialGiftID *int64
|
||||
SourceManifestSha256 []byte
|
||||
}
|
||||
|
||||
type StarGiftCollection struct {
|
||||
CollectionID int32
|
||||
OwnerPeerType string
|
||||
OwnerPeerID int64
|
||||
Title string
|
||||
SortOrder int32
|
||||
Hash int64
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftCollectionItem struct {
|
||||
CollectionID int32
|
||||
SavedGiftID int64
|
||||
SortOrder int32
|
||||
CreatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftConversion struct {
|
||||
SavedGiftID int64
|
||||
ActorUserID int64
|
||||
OwnerPeerType string
|
||||
OwnerPeerID int64
|
||||
Amount int64
|
||||
BalanceAfter int64
|
||||
ConvertedAt int32
|
||||
}
|
||||
|
||||
type StarGiftCraftCommand struct {
|
||||
UserID int64
|
||||
CommandKey string
|
||||
InputUniqueGiftIds []int64
|
||||
GiftID int64
|
||||
Success bool
|
||||
ResultUniqueGiftID *int64
|
||||
ChancePermille int32
|
||||
CreatedAt int32
|
||||
SourceEditPts []int32
|
||||
}
|
||||
|
||||
type StarGiftDropDetailsCommand struct {
|
||||
UserID int64
|
||||
CommandKey string
|
||||
SavedGiftID int64
|
||||
UniqueGiftID int64
|
||||
FormID int64
|
||||
ChargeStars int64
|
||||
BalanceAfter int64
|
||||
CreatedAt int32
|
||||
}
|
||||
|
||||
type StarGiftListing struct {
|
||||
UniqueGiftID int64
|
||||
SellerPeerType string
|
||||
SellerPeerID int64
|
||||
Currency string
|
||||
Amount int64
|
||||
ListedAt int32
|
||||
UpdatedAt int32
|
||||
Version int64
|
||||
}
|
||||
|
||||
type StarGiftNotificationSetting struct {
|
||||
UserID int64
|
||||
ChannelID int64
|
||||
Enabled bool
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftOffer struct {
|
||||
ID int64
|
||||
BuyerUserID int64
|
||||
OwnerPeerType string
|
||||
OwnerPeerID int64
|
||||
UniqueGiftID int64
|
||||
Currency string
|
||||
Amount int64
|
||||
RandomID int64
|
||||
OfferMsgID int32
|
||||
BuyerMsgID int32
|
||||
Status string
|
||||
CreatedAt int32
|
||||
ExpiresAt int32
|
||||
ResolvedAt int32
|
||||
BalanceAfter int64
|
||||
ResolutionNotified bool
|
||||
}
|
||||
|
||||
type StarGiftPrepaidUpgradeCommand struct {
|
||||
PayerUserID int64
|
||||
CommandKey string
|
||||
SavedGiftID int64
|
||||
FormID int64
|
||||
ChargeStars int64
|
||||
BalanceAfter int64
|
||||
CreatedAt int32
|
||||
}
|
||||
|
||||
type StarGiftPurchaseCommand struct {
|
||||
BuyerUserID int64
|
||||
CommandKey string
|
||||
GiftID int64
|
||||
RecipientPeerType string
|
||||
RecipientPeerID int64
|
||||
SavedGiftID int64
|
||||
FormID int64
|
||||
ChargeStars int64
|
||||
BalanceAfter int64
|
||||
CreatedAt int32
|
||||
}
|
||||
|
||||
type StarGiftPurchaseForm struct {
|
||||
BuyerUserID int64
|
||||
FormID int64
|
||||
GiftID int64
|
||||
RevisionID int64
|
||||
RecipientPeerType string
|
||||
RecipientPeerID int64
|
||||
IncludeUpgrade bool
|
||||
HideName bool
|
||||
Message string
|
||||
ChargeStars int64
|
||||
IssuedAt int32
|
||||
ExpiresAt int32
|
||||
}
|
||||
|
||||
type StarGiftSale struct {
|
||||
ID int64
|
||||
UniqueGiftID int64
|
||||
SellerPeerType string
|
||||
SellerPeerID int64
|
||||
BuyerPeerType string
|
||||
BuyerPeerID int64
|
||||
Currency string
|
||||
Amount int64
|
||||
CommissionAmount int64
|
||||
SoldAt int32
|
||||
CommandKey string
|
||||
}
|
||||
|
||||
type StarGiftTransferCommand struct {
|
||||
ActorUserID int64
|
||||
CommandKey string
|
||||
UniqueGiftID int64
|
||||
FromPeerType string
|
||||
FromPeerID int64
|
||||
ToPeerType string
|
||||
ToPeerID int64
|
||||
ChargeStars int64
|
||||
BalanceAfter int64
|
||||
CreatedAt int32
|
||||
}
|
||||
|
||||
type StarGiftUpgradeCommand struct {
|
||||
UserID int64
|
||||
CommandKey string
|
||||
SourceSavedGiftID int64
|
||||
FormID int64
|
||||
UniqueGiftID int64
|
||||
BalanceAfter int64
|
||||
CreatedAt pgtype.Timestamptz
|
||||
ChargeStars int64
|
||||
RequirePrepaid bool
|
||||
KeepOriginalDetails bool
|
||||
SourceEditPts int32
|
||||
}
|
||||
|
||||
type StarGiftUserPurchase struct {
|
||||
UserID int64
|
||||
GiftID int64
|
||||
PurchasedCount int32
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type StarGiftWithdrawalRequest struct {
|
||||
ID int64
|
||||
UniqueGiftID int64
|
||||
OwnerUserID int64
|
||||
Provider string
|
||||
ProviderRequestID string
|
||||
Url string
|
||||
Status string
|
||||
CreatedAt int32
|
||||
ExpiresAt int32
|
||||
CompletedAt int32
|
||||
}
|
||||
|
||||
type StarsBalance struct {
|
||||
UserID int64
|
||||
Balance int64
|
||||
|
|
@ -1564,6 +2017,66 @@ type ThemeUserInstall struct {
|
|||
InstalledAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type TonBalance struct {
|
||||
UserID int64
|
||||
BalanceNanoton int64
|
||||
Granted bool
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type TonTransaction struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
AmountNanoton int64
|
||||
Reason string
|
||||
PeerType *string
|
||||
PeerID *int64
|
||||
GiftID *int64
|
||||
Date int32
|
||||
}
|
||||
|
||||
type UniqueStarGift struct {
|
||||
ID int64
|
||||
GiftID int64
|
||||
CollectibleRevisionID int64
|
||||
SourceSavedGiftID int64
|
||||
Title string
|
||||
Slug string
|
||||
Num int32
|
||||
OwnerPeerType *string
|
||||
OwnerPeerID *int64
|
||||
ModelAttributeID int64
|
||||
PatternAttributeID int64
|
||||
BackdropAttributeID int64
|
||||
KeepOriginalDetails bool
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
RequirePremium bool
|
||||
ResaleTonOnly bool
|
||||
ThemeAvailable bool
|
||||
Burned bool
|
||||
Crafted bool
|
||||
OriginalOwnerPeerType string
|
||||
OriginalOwnerPeerID int64
|
||||
OwnerName string
|
||||
OwnerAddress string
|
||||
GiftAddress string
|
||||
ReleasedByPeerType *string
|
||||
ReleasedByPeerID *int64
|
||||
ValueAmount int64
|
||||
ValueCurrency string
|
||||
ValueUsdAmount int64
|
||||
ThemePeerType *string
|
||||
ThemePeerID *int64
|
||||
HostPeerType *string
|
||||
HostPeerID *int64
|
||||
OfferMinStars int32
|
||||
CraftChancePermille int32
|
||||
LastSaleDate int32
|
||||
LastSaleCurrency string
|
||||
LastSaleAmount int64
|
||||
}
|
||||
|
||||
type UpdateState struct {
|
||||
AuthKeyID int64
|
||||
Pts int32
|
||||
|
|
@ -1627,6 +2140,10 @@ type User struct {
|
|||
BirthdayMonth int32
|
||||
BirthdayYear int32
|
||||
PersonalChannelID int64
|
||||
DeletedAt pgtype.Timestamptz
|
||||
DeletionSource string
|
||||
DeletionReason string
|
||||
AccountDeleteAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type UserBusinessProfile struct {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import (
|
|||
const createUser = `-- name: CreateUser :one
|
||||
INSERT INTO users (access_hash, phone, first_name, last_name, username, country_code, premium_expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type CreateUserParams struct {
|
||||
|
|
@ -68,12 +68,16 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByID = `-- name: GetUserByID :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id FROM users WHERE id = $1
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at FROM users WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||
|
|
@ -109,12 +113,16 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByPhone = `-- name: GetUserByPhone :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id FROM users WHERE phone = $1
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at FROM users WHERE phone = $1 AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByPhone(ctx context.Context, phone string) (User, error) {
|
||||
|
|
@ -150,12 +158,16 @@ func (q *Queries) GetUserByPhone(ctx context.Context, phone string) (User, error
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id FROM users WHERE lower(username) = lower($1) AND username <> ''
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at FROM users WHERE lower(username) = lower($1) AND username <> '' AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserByUsername(ctx context.Context, lower string) (User, error) {
|
||||
|
|
@ -191,12 +203,16 @@ func (q *Queries) GetUserByUsername(ctx context.Context, lower string) (User, er
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUsersByIDs = `-- name: GetUsersByIDs :many
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
FROM users
|
||||
WHERE id = ANY($1::bigint[])
|
||||
ORDER BY id
|
||||
|
|
@ -241,6 +257,10 @@ func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -253,9 +273,9 @@ func (q *Queries) GetUsersByIDs(ctx context.Context, ids []int64) ([]User, error
|
|||
}
|
||||
|
||||
const getUsersByPhones = `-- name: GetUsersByPhones :many
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
SELECT id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
FROM users
|
||||
WHERE phone = ANY($1::text[])
|
||||
WHERE phone = ANY($1::text[]) AND deleted_at IS NULL
|
||||
ORDER BY id
|
||||
`
|
||||
|
||||
|
|
@ -298,6 +318,10 @@ func (q *Queries) GetUsersByPhones(ctx context.Context, phones []string) ([]User
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -347,6 +371,7 @@ WITH matched AS (
|
|||
FROM users u
|
||||
LEFT JOIN contacts c ON c.user_id = $4::bigint AND c.contact_user_id = u.id
|
||||
WHERE u.id <> $4::bigint
|
||||
AND u.deleted_at IS NULL
|
||||
AND $3::text <> ''
|
||||
AND (
|
||||
($2::text <> '' AND u.phone LIKE $2::text || '%')
|
||||
|
|
@ -479,8 +504,8 @@ const setUserPremiumUntil = `-- name: SetUserPremiumUntil :one
|
|||
UPDATE users
|
||||
SET premium_expires_at = $1::timestamptz,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $2::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type SetUserPremiumUntilParams struct {
|
||||
|
|
@ -521,6 +546,10 @@ func (q *Queries) SetUserPremiumUntil(ctx context.Context, arg SetUserPremiumUnt
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -529,8 +558,8 @@ const setUserVerified = `-- name: SetUserVerified :one
|
|||
UPDATE users
|
||||
SET verified = $1::boolean,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $2::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type SetUserVerifiedParams struct {
|
||||
|
|
@ -571,6 +600,10 @@ func (q *Queries) SetUserVerified(ctx context.Context, arg SetUserVerifiedParams
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -582,11 +615,12 @@ SET premium_expires_at = NULL,
|
|||
WHERE id IN (
|
||||
SELECT id FROM users
|
||||
WHERE premium_expires_at IS NOT NULL
|
||||
AND deleted_at IS NULL
|
||||
AND premium_expires_at <= $1::timestamptz
|
||||
ORDER BY premium_expires_at
|
||||
LIMIT $2::int
|
||||
)
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type SweepExpiredPremiumParams struct {
|
||||
|
|
@ -633,6 +667,10 @@ func (q *Queries) SweepExpiredPremium(ctx context.Context, arg SweepExpiredPremi
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -650,8 +688,8 @@ SET birthday_day = $1::int,
|
|||
birthday_month = $2::int,
|
||||
birthday_year = $3::int,
|
||||
updated_at = now()
|
||||
WHERE id = $4::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $4::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type UpdateUserBirthdayParams struct {
|
||||
|
|
@ -699,6 +737,10 @@ func (q *Queries) UpdateUserBirthday(ctx context.Context, arg UpdateUserBirthday
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -709,8 +751,8 @@ SET color_set = $1::boolean,
|
|||
color = $2::int,
|
||||
color_background_emoji_id = $3::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $4::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $4::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type UpdateUserColorParams struct {
|
||||
|
|
@ -758,6 +800,10 @@ func (q *Queries) UpdateUserColor(ctx context.Context, arg UpdateUserColorParams
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -767,8 +813,8 @@ UPDATE users
|
|||
SET emoji_status_document_id = $1::bigint,
|
||||
emoji_status_until = $2::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $3::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $3::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type UpdateUserEmojiStatusParams struct {
|
||||
|
|
@ -810,6 +856,10 @@ func (q *Queries) UpdateUserEmojiStatus(ctx context.Context, arg UpdateUserEmoji
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -818,7 +868,7 @@ const updateUserLastSeen = `-- name: UpdateUserLastSeen :exec
|
|||
UPDATE users
|
||||
SET last_seen_at = GREATEST(last_seen_at, $1::bigint),
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint
|
||||
WHERE id = $2::bigint AND deleted_at IS NULL
|
||||
`
|
||||
|
||||
type UpdateUserLastSeenParams struct {
|
||||
|
|
@ -835,8 +885,8 @@ const updateUserPersonalChannel = `-- name: UpdateUserPersonalChannel :one
|
|||
UPDATE users
|
||||
SET personal_channel_id = $1::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $2::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type UpdateUserPersonalChannelParams struct {
|
||||
|
|
@ -877,6 +927,10 @@ func (q *Queries) UpdateUserPersonalChannel(ctx context.Context, arg UpdateUserP
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -885,8 +939,8 @@ const updateUserPhone = `-- name: UpdateUserPhone :one
|
|||
UPDATE users
|
||||
SET phone = $1::text,
|
||||
updated_at = now()
|
||||
WHERE id = $2::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $2::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type UpdateUserPhoneParams struct {
|
||||
|
|
@ -927,6 +981,10 @@ func (q *Queries) UpdateUserPhone(ctx context.Context, arg UpdateUserPhoneParams
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -937,8 +995,8 @@ SET first_name = $2,
|
|||
last_name = $3,
|
||||
about = $4,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type UpdateUserProfileParams struct {
|
||||
|
|
@ -986,6 +1044,10 @@ func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfilePa
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -996,8 +1058,8 @@ SET profile_color_set = $1::boolean,
|
|||
profile_color = $2::int,
|
||||
profile_color_background_emoji_id = $3::bigint,
|
||||
updated_at = now()
|
||||
WHERE id = $4::bigint
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $4::bigint AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type UpdateUserProfileColorParams struct {
|
||||
|
|
@ -1045,6 +1107,10 @@ func (q *Queries) UpdateUserProfileColor(ctx context.Context, arg UpdateUserProf
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
@ -1053,8 +1119,8 @@ const updateUserUsername = `-- name: UpdateUserUsername :one
|
|||
UPDATE users
|
||||
SET username = $2,
|
||||
updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id
|
||||
WHERE id = $1 AND deleted_at IS NULL
|
||||
RETURNING id, access_hash, phone, first_name, last_name, username, country_code, created_at, updated_at, verified, support, about, last_seen_at, default_history_ttl_period, is_bot, bot_info_version, premium_expires_at, emoji_status_document_id, emoji_status_until, color_set, color, color_background_emoji_id, profile_color_set, profile_color, profile_color_background_emoji_id, birthday_day, birthday_month, birthday_year, personal_channel_id, deleted_at, deletion_source, deletion_reason, account_delete_at
|
||||
`
|
||||
|
||||
type UpdateUserUsernameParams struct {
|
||||
|
|
@ -1095,6 +1161,10 @@ func (q *Queries) UpdateUserUsername(ctx context.Context, arg UpdateUserUsername
|
|||
&i.BirthdayMonth,
|
||||
&i.BirthdayYear,
|
||||
&i.PersonalChannelID,
|
||||
&i.DeletedAt,
|
||||
&i.DeletionSource,
|
||||
&i.DeletionReason,
|
||||
&i.AccountDeleteAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func TestStarGiftLifecycleMigrationsApply(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("migrate star gift lifecycle schema: %v", err)
|
||||
}
|
||||
if status.Dirty || status.Empty || status.Version != 106 {
|
||||
t.Fatalf("migration status = %+v, want clean version 106", status)
|
||||
if status.Dirty || status.Empty || status.Version != 107 {
|
||||
t.Fatalf("migration status = %+v, want clean version 107", status)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ func (s *UserStore) UpdateUsername(ctx context.Context, userID int64, username s
|
|||
}()
|
||||
qtx := s.q.WithTx(tx)
|
||||
var lockedUserID int64
|
||||
if err := tx.QueryRow(ctx, `SELECT id FROM users WHERE id = $1 FOR UPDATE`, userID).Scan(&lockedUserID); err != nil {
|
||||
if err := tx.QueryRow(ctx, `SELECT id FROM users WHERE id = $1 AND deleted_at IS NULL FOR UPDATE`, userID).Scan(&lockedUserID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.User{}, domain.ErrUsernameNotOccupied
|
||||
}
|
||||
|
|
@ -444,7 +444,7 @@ func escapeLike(s string) string {
|
|||
}
|
||||
|
||||
func userFromModel(r sqlcgen.User) domain.User {
|
||||
return domain.User{
|
||||
u := domain.User{
|
||||
ID: r.ID,
|
||||
AccessHash: r.AccessHash,
|
||||
Phone: r.Phone,
|
||||
|
|
@ -465,7 +465,17 @@ func userFromModel(r sqlcgen.User) domain.User {
|
|||
Color: peerColorFromModel(r.ColorSet, r.Color, r.ColorBackgroundEmojiID),
|
||||
ProfileColor: peerColorFromModel(r.ProfileColorSet, r.ProfileColor, r.ProfileColorBackgroundEmojiID),
|
||||
LastSeenAt: int(r.LastSeenAt),
|
||||
Deleted: r.DeletedAt.Valid,
|
||||
DeletionSource: domain.AccountDeletionSource(r.DeletionSource),
|
||||
DeletionReason: r.DeletionReason,
|
||||
CreatedAt: r.CreatedAt.Time,
|
||||
AccountDeleteAt: r.AccountDeleteAt.Time,
|
||||
}
|
||||
if r.DeletedAt.Valid {
|
||||
u.DeletedAt = r.DeletedAt.Time.Unix()
|
||||
return u.DeletedTombstone()
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func peerColorFromModel(hasColor bool, color int32, backgroundEmojiID int64) domain.PeerColor {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue