diplomail (Stage C): paid-tier broadcast + multi-game + cleanup
Closes out the producer-side of the diplomail surface. Paid-tier players can fan out one personal message to the rest of the active roster (gated on entitlement_snapshots.is_paid). Site admins gain a multi-game broadcast (POST /admin/mail/broadcast with `selected` / `all_running` scopes) and the bulk-purge endpoint that wipes diplomail rows tied to games finished more than N years ago. An admin listing (GET /admin/mail/messages) rounds out the observability surface. EntitlementReader and GameLookup are new narrow deps wired from `*user.Service` and `*lobby.Service` in cmd/backend/main; the lobby service grows a one-off `ListFinishedGamesBefore` helper for the cleanup path (the cache evicts terminal-state games so the cache walk is not enough). Stage D will swap LangUndetermined for an actual body-language detector and add the translation cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1183,6 +1183,45 @@ paths:
|
||||
$ref: "#/components/responses/NotImplementedError"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/user/games/{game_id}/mail/broadcast:
|
||||
post:
|
||||
tags: [User]
|
||||
operationId: userMailSendBroadcast
|
||||
summary: Send a paid-tier personal broadcast to a game's active members
|
||||
description: |
|
||||
Paid-tier players (`entitlement.is_paid == true`) may send one
|
||||
personal message that fans out to every other active member of
|
||||
the game. Free-tier callers receive 403. The resulting rows
|
||||
carry `kind="personal"`, `sender_kind="player"`,
|
||||
`broadcast_scope="game_broadcast"`. Recipients reply through
|
||||
the regular personal-send endpoint; the reply targets the
|
||||
broadcaster only.
|
||||
security:
|
||||
- UserHeader: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/XUserID"
|
||||
- $ref: "#/components/parameters/GameID"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserMailSendBroadcastRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Personal broadcast accepted; receipt carries the recipient count.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/UserMailBroadcastReceipt"
|
||||
"400":
|
||||
$ref: "#/components/responses/InvalidRequestError"
|
||||
"403":
|
||||
$ref: "#/components/responses/ForbiddenError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplementedError"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/user/games/{game_id}/mail/admin:
|
||||
post:
|
||||
tags: [User]
|
||||
@@ -1954,6 +1993,133 @@ paths:
|
||||
$ref: "#/components/responses/NotImplementedError"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/admin/mail/broadcast:
|
||||
post:
|
||||
tags: [Admin]
|
||||
operationId: adminDiplomailBroadcast
|
||||
summary: Multi-game admin broadcast
|
||||
description: |
|
||||
Fans out one admin-kind broadcast across the games selected
|
||||
by `scope`. `scope="selected"` requires `game_ids`;
|
||||
`scope="all_running"` enumerates every game whose status is
|
||||
non-terminal. Recipients are resolved per-game via the same
|
||||
scope vocabulary as the per-game admin send. A recipient
|
||||
appearing in multiple addressed games receives one
|
||||
independently-deletable inbox entry per game.
|
||||
security:
|
||||
- AdminBasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AdminDiplomailBroadcastRequest"
|
||||
responses:
|
||||
"201":
|
||||
description: Broadcast accepted; per-game message ids and total recipient count.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AdminDiplomailBroadcastResponse"
|
||||
"400":
|
||||
$ref: "#/components/responses/InvalidRequestError"
|
||||
"401":
|
||||
$ref: "#/components/responses/UnauthorizedError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplementedError"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/admin/mail/cleanup:
|
||||
post:
|
||||
tags: [Admin]
|
||||
operationId: adminDiplomailCleanup
|
||||
summary: Bulk-purge diplomail messages from old finished games
|
||||
description: |
|
||||
Removes every `diplomail_messages` row whose game finished
|
||||
more than `older_than_years` years ago. Cascading FKs prune
|
||||
the recipient and translation tables in the same transaction.
|
||||
`older_than_years` must be >= 1.
|
||||
security:
|
||||
- AdminBasicAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AdminDiplomailCleanupRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: Cleanup result.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AdminDiplomailCleanupResponse"
|
||||
"400":
|
||||
$ref: "#/components/responses/InvalidRequestError"
|
||||
"401":
|
||||
$ref: "#/components/responses/UnauthorizedError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplementedError"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/admin/mail/messages:
|
||||
get:
|
||||
tags: [Admin]
|
||||
operationId: adminDiplomailList
|
||||
summary: Paginated admin view of diplomail messages
|
||||
description: |
|
||||
Returns the canonical message rows for admin observability.
|
||||
Optional filters: `game_id`, `kind` (personal / admin),
|
||||
`sender_kind` (player / admin / system). Pagination via
|
||||
`page` and `page_size`.
|
||||
security:
|
||||
- AdminBasicAuth: []
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
- name: page_size
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
- name: game_id
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: kind
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [personal, admin]
|
||||
- name: sender_kind
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [player, admin, system]
|
||||
responses:
|
||||
"200":
|
||||
description: Paginated diplomail messages.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AdminDiplomailListResponse"
|
||||
"400":
|
||||
$ref: "#/components/responses/InvalidRequestError"
|
||||
"401":
|
||||
$ref: "#/components/responses/UnauthorizedError"
|
||||
"501":
|
||||
$ref: "#/components/responses/NotImplementedError"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/admin/games/{game_id}/mail:
|
||||
post:
|
||||
tags: [Admin]
|
||||
@@ -3983,6 +4149,145 @@ components:
|
||||
recipient_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
UserMailSendBroadcastRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [body]
|
||||
properties:
|
||||
subject:
|
||||
type: string
|
||||
body:
|
||||
type: string
|
||||
AdminDiplomailBroadcastRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [scope, body]
|
||||
properties:
|
||||
scope:
|
||||
type: string
|
||||
enum: [selected, all_running]
|
||||
game_ids:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
recipients:
|
||||
type: string
|
||||
enum: [active, active_and_removed, all_members]
|
||||
subject:
|
||||
type: string
|
||||
body:
|
||||
type: string
|
||||
AdminDiplomailBroadcastResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [recipient_count, messages]
|
||||
properties:
|
||||
recipient_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [message_id, game_id]
|
||||
properties:
|
||||
message_id:
|
||||
type: string
|
||||
format: uuid
|
||||
game_id:
|
||||
type: string
|
||||
format: uuid
|
||||
game_name:
|
||||
type: string
|
||||
AdminDiplomailCleanupRequest:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [older_than_years]
|
||||
properties:
|
||||
older_than_years:
|
||||
type: integer
|
||||
minimum: 1
|
||||
AdminDiplomailCleanupResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [messages_deleted, game_ids]
|
||||
properties:
|
||||
messages_deleted:
|
||||
type: integer
|
||||
minimum: 0
|
||||
game_ids:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
AdminDiplomailMessage:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- message_id
|
||||
- game_id
|
||||
- kind
|
||||
- sender_kind
|
||||
- body
|
||||
- body_lang
|
||||
- broadcast_scope
|
||||
- created_at
|
||||
properties:
|
||||
message_id:
|
||||
type: string
|
||||
format: uuid
|
||||
game_id:
|
||||
type: string
|
||||
format: uuid
|
||||
game_name:
|
||||
type: string
|
||||
kind:
|
||||
type: string
|
||||
enum: [personal, admin]
|
||||
sender_kind:
|
||||
type: string
|
||||
enum: [player, admin, system]
|
||||
sender_user_id:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
sender_username:
|
||||
type: string
|
||||
nullable: true
|
||||
sender_ip:
|
||||
type: string
|
||||
subject:
|
||||
type: string
|
||||
body:
|
||||
type: string
|
||||
body_lang:
|
||||
type: string
|
||||
broadcast_scope:
|
||||
type: string
|
||||
enum: [single, game_broadcast, multi_game_broadcast]
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
AdminDiplomailListResponse:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [total, page, page_size, items]
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
minimum: 0
|
||||
page:
|
||||
type: integer
|
||||
minimum: 1
|
||||
page_size:
|
||||
type: integer
|
||||
minimum: 1
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/AdminDiplomailMessage"
|
||||
UserMailMessageDetail:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
|
||||
Reference in New Issue
Block a user