feat(accountdelete): drop all-robot games + unspoofable [Deleted] label
Q2=B: DropAllRobotGames deletes the deletee's games with no human opponent (vs-AI or auto-match-robot; children cascade), keeping games with any human seat (anonymized instead). Q1=A: the anon label is the sentinel [Deleted] — the editable-name rule forbids brackets, so no live player can impersonate a deleted account. Integration test covers drop-vs-keep.
This commit is contained in:
@@ -25,8 +25,10 @@ import (
|
||||
|
||||
// AnonymizedName is the label a deleted account shows to opponents. Display names are
|
||||
// stored strings resolved identically for every viewer (no per-viewer localisation in this
|
||||
// codebase), so a single canonical label is used.
|
||||
const AnonymizedName = "Удалённый игрок"
|
||||
// codebase), so a single canonical label is used. The brackets are deliberate: the
|
||||
// editable-name rule (account.displayNameRe) forbids them, so a live player can never set a
|
||||
// name that impersonates a deleted account.
|
||||
const AnonymizedName = "[Deleted]"
|
||||
|
||||
// retainDelete is the retained_identities reason written when a credential is journalled
|
||||
// because its account is being deleted.
|
||||
@@ -70,6 +72,38 @@ func (d *Deleter) AnonymizeAndTombstone(ctx context.Context, accountID uuid.UUID
|
||||
})
|
||||
}
|
||||
|
||||
// dropAllRobotGamesSQL deletes every game in which the account plays and no other seat is a
|
||||
// human — a robot seat is one whose account holds a 'robot' identity, so this covers both
|
||||
// honest vs-AI games and disguised auto-match substitutes. The game rows are deleted; their
|
||||
// moves/chat/players/complaints fall away through ON DELETE CASCADE.
|
||||
const dropAllRobotGamesSQL = `
|
||||
DELETE FROM games g
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM game_players p WHERE p.game_id = g.game_id AND p.account_id = $1
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1 FROM game_players o
|
||||
WHERE o.game_id = g.game_id AND o.account_id <> $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM identities i WHERE i.account_id = o.account_id AND i.kind = 'robot'
|
||||
)
|
||||
)`
|
||||
|
||||
// DropAllRobotGames deletes the account's games that have no human opponent (solo vs-AI or
|
||||
// auto-match-robot games), returning how many were removed. Games with any human seat are
|
||||
// kept — their seat is anonymised by AnonymizeAndTombstone instead. Run it after the
|
||||
// account's active games are resigned, so no live game is removed under the robot driver.
|
||||
func (d *Deleter) DropAllRobotGames(ctx context.Context, accountID uuid.UUID) (int64, error) {
|
||||
res, err := d.db.ExecContext(ctx, dropAllRobotGamesSQL, accountID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("accountdelete: drop all-robot games: %w", err)
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("accountdelete: dropped games count: %w", err)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// journalAndDropIdentities copies the account's live identities into the retention journal
|
||||
// (reason=delete) and then removes them, freeing each (kind, external_id) for reuse.
|
||||
func journalAndDropIdentities(ctx context.Context, tx *sql.Tx, accountID uuid.UUID, now time.Time) error {
|
||||
|
||||
Reference in New Issue
Block a user