feat(social): hide social controls on a deleted opponent's seat
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 29s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m17s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m49s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 29s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m17s
CI / conformance (pull_request) Successful in 11s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m49s
A deleted account keeps its seats in every shared game, so its opponents
still saw the add-friend and block controls on the scoreboard (deletion
drops the friendship, so the 🤝 even reappeared for a former friend) and
a chat composer nobody was behind. A friend request sent that way was
accepted by the server and stayed pending forever.
The per-viewer game views now mark such a seat (SeatView.deleted,
resolved beside the seat display names by a batch accounts.deleted_at
lookup) and the client hides every control aimed at it: add-friend,
block, and the chat composer (message + nudge) once no reachable
opponent is left. Live events carry the game domain's seat standings and
so leave the mark unset, so the delta reducers preserve the cached one.
SendFriendRequest and Block against a tombstone are refused with
social.ErrAccountDeleted (410 account_deleted) — the source of truth for
an older client.
This commit is contained in:
@@ -71,6 +71,11 @@ type Account struct {
|
||||
// uuid.Nil for a live account. A tombstone keeps the row so the no-cascade
|
||||
// foreign keys of a shared finished game stay valid.
|
||||
MergedInto uuid.UUID
|
||||
// Deleted marks a tombstoned account (accounts.deleted_at set): its credentials are
|
||||
// journalled and freed and its live surfaces anonymised, but the row survives for the
|
||||
// no-cascade foreign keys. Nobody is behind it any more, so it takes part in no social
|
||||
// exchange (docs/ARCHITECTURE.md §9.1).
|
||||
Deleted bool
|
||||
// FlaggedHighRateAt is the soft, reversible "suspected high-rate" marker: the
|
||||
// zero time for an unflagged account, otherwise when the gateway-reported
|
||||
// rate-limiter rejections first crossed the sustained threshold. An
|
||||
@@ -630,6 +635,7 @@ func modelToAccount(row model.Accounts) Account {
|
||||
IsGuest: row.IsGuest,
|
||||
NotificationsInAppOnly: row.NotificationsInAppOnly,
|
||||
MergedInto: mergedInto,
|
||||
Deleted: row.DeletedAt != nil,
|
||||
FlaggedHighRateAt: flaggedHighRateAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
|
||||
@@ -180,6 +180,32 @@ func (s *Store) DeletionInfo(ctx context.Context, accountID uuid.UUID) (Deletion
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// DeletedIDs returns the subset of ids whose accounts are tombstoned (deleted_at set), as a
|
||||
// set. It is the batch form of Account.Deleted for a caller holding several ids at once —
|
||||
// the game views resolve every seat's deleted mark in one round-trip. An empty ids slice
|
||||
// queries nothing and returns an empty set; an unknown id is simply absent from the result.
|
||||
func (s *Store) DeletedIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]bool, error) {
|
||||
out := map[uuid.UUID]bool{}
|
||||
if len(ids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
exprs := make([]postgres.Expression, len(ids))
|
||||
for i, id := range ids {
|
||||
exprs[i] = postgres.UUID(id)
|
||||
}
|
||||
stmt := postgres.SELECT(table.Accounts.AccountID).
|
||||
FROM(table.Accounts).
|
||||
WHERE(table.Accounts.AccountID.IN(exprs...).AND(table.Accounts.DeletedAt.IS_NOT_NULL()))
|
||||
var rows []model.Accounts
|
||||
if err := stmt.QueryContext(ctx, s.db, &rows); err != nil {
|
||||
return nil, fmt.Errorf("account: deleted ids: %w", err)
|
||||
}
|
||||
for _, r := range rows {
|
||||
out[r.AccountID] = true
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// RetentionReaper periodically purges expired account-deletion retention data via
|
||||
// Store.ReapExpiredRetention, mirroring GuestReaper: one background goroutine started once
|
||||
// from main.
|
||||
|
||||
Reference in New Issue
Block a user