feat(multiplayer): enforce proposal decline cooldowns

This commit is contained in:
Josh Creek
2026-09-01 19:30:45 +01:00
parent da92be73ef
commit 455055c67c
6 changed files with 68 additions and 0 deletions
+43
View File
@@ -129,6 +129,46 @@ SET state = 'QUEUED', expires_at = $2, revision = revision + 1
FROM proposal_participants pp
WHERE pp.proposal_id = $1 AND q.ticket_id = pp.ticket_id AND q.player_id = pp.player_id AND q.state = 'PROPOSED'`
const ProposalCooldownEventsSQL = `SELECT kind, starts_at
FROM penalties
WHERE player_id = $1 AND playlist = $2
AND kind IN ('PROPOSAL_DECLINED', 'PROPOSAL_TIMEOUT')
AND starts_at >= $3
ORDER BY starts_at`
const ProposalCooldownInsertSQL = `INSERT INTO penalties
(penalty_id, player_id, playlist, kind, starts_at, ends_at)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (penalty_id) DO NOTHING`
func recordProposalDeclineCooldown(ctx context.Context, tx *sql.Tx, playerID string, playlist domain.Playlist, proposalID string, now time.Time) error {
rows, err := tx.QueryContext(ctx, ProposalCooldownEventsSQL, playerID, string(playlist), now.Add(-30*time.Minute))
if err != nil {
return err
}
defer rows.Close()
events := make([]domain.CooldownEvent, 0)
for rows.Next() {
var kind string
var at time.Time
if err := rows.Scan(&kind, &at); err != nil {
return err
}
response := domain.TimedOutResponse
if kind == "PROPOSAL_DECLINED" {
response = domain.DeclinedResponse
}
events = append(events, domain.CooldownEvent{At: at, Playlist: playlist, Kind: response})
}
if err := rows.Err(); err != nil {
return err
}
events = append(events, domain.CooldownEvent{At: now, Playlist: playlist, Kind: domain.DeclinedResponse})
until := domain.CooldownUntil(events, playlist, now)
_, err = tx.ExecContext(ctx, ProposalCooldownInsertSQL, "proposal-decline:"+proposalID+":"+playerID, playerID, string(playlist), "PROPOSAL_DECLINED", now, until)
return err
}
const ProposalRevisionBumpSQL = `UPDATE proposals
SET revision = revision + 1
WHERE proposal_id = $1 AND state = 'OPEN'`
@@ -286,6 +326,9 @@ func RespondToProposal(ctx context.Context, db *sql.DB, playerID, proposalID, id
if err != nil {
return err
}
if err := recordProposalDeclineCooldown(ctx, tx, playerID, domain.Playlist(playlist), proposalID, now); err != nil {
return err
}
_, err = tx.ExecContext(ctx, ProposalDeclineRequeueSQL, proposalID, now.Add(domain.QueueExpiryWindow))
}
if err != nil {