Release v1.8.0: promote development → master #173
@@ -25,8 +25,10 @@ import (
|
|||||||
|
|
||||||
// AnonymizedName is the label a deleted account shows to opponents. Display names are
|
// 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
|
// stored strings resolved identically for every viewer (no per-viewer localisation in this
|
||||||
// codebase), so a single canonical label is used.
|
// codebase), so a single canonical label is used. The brackets are deliberate: the
|
||||||
const AnonymizedName = "Удалённый игрок"
|
// 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
|
// retainDelete is the retained_identities reason written when a credential is journalled
|
||||||
// because its account is being deleted.
|
// 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
|
// journalAndDropIdentities copies the account's live identities into the retention journal
|
||||||
// (reason=delete) and then removes them, freeing each (kind, external_id) for reuse.
|
// (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 {
|
func journalAndDropIdentities(ctx context.Context, tx *sql.Tx, accountID uuid.UUID, now time.Time) error {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
|
|
||||||
"scrabble/backend/internal/account"
|
"scrabble/backend/internal/account"
|
||||||
"scrabble/backend/internal/accountdelete"
|
"scrabble/backend/internal/accountdelete"
|
||||||
|
"scrabble/backend/internal/engine"
|
||||||
|
"scrabble/backend/internal/game"
|
||||||
)
|
)
|
||||||
|
|
||||||
// deletedFields reads a tombstoned account's retained real name and its deleted_at.
|
// deletedFields reads a tombstoned account's retained real name and its deleted_at.
|
||||||
@@ -90,3 +92,44 @@ func TestAnonymizeAndTombstone(t *testing.T) {
|
|||||||
t.Fatalf("email should be free after deletion, got: %v", err)
|
t.Fatalf("email should be free after deletion, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDropAllRobotGames drops the deletee's solo vs-AI game but keeps a game with a human
|
||||||
|
// opponent.
|
||||||
|
func TestDropAllRobotGames(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
gsvc := newGameService()
|
||||||
|
robots := newRobotService(t, gsvc)
|
||||||
|
if err := robots.EnsurePool(ctx); err != nil {
|
||||||
|
t.Fatalf("ensure pool: %v", err)
|
||||||
|
}
|
||||||
|
mm := newMatchmaker(t, robots, time.Minute, 0)
|
||||||
|
deleter := accountdelete.NewDeleter(testDB)
|
||||||
|
|
||||||
|
user := provisionAccount(t)
|
||||||
|
other := provisionAccount(t)
|
||||||
|
|
||||||
|
aiRes, err := mm.StartVsAI(ctx, user, engine.VariantEnglish, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("start vs AI: %v", err)
|
||||||
|
}
|
||||||
|
humanGame, err := gsvc.Create(ctx, game.CreateParams{
|
||||||
|
Variant: engine.VariantEnglish, Seats: []uuid.UUID{user, other}, TurnTimeout: time.Hour, Seed: 1,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create human game: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := deleter.DropAllRobotGames(ctx, user)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("drop: %v", err)
|
||||||
|
}
|
||||||
|
if n != 1 {
|
||||||
|
t.Fatalf("dropped %d games, want 1 (the vs-AI game)", n)
|
||||||
|
}
|
||||||
|
if _, err := gsvc.GameByID(ctx, aiRes.Game.ID); err == nil {
|
||||||
|
t.Error("the vs-AI game should be dropped")
|
||||||
|
}
|
||||||
|
if _, err := gsvc.GameByID(ctx, humanGame.ID); err != nil {
|
||||||
|
t.Errorf("the human game should be kept, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user