Goal in one sentence: a player presses "request replay review" in the client, and the request appears as a post in the training Discord's replay review forum channel, attributed to a FAF account the server vouched for.
This writes up Brutus5000's suggestion from Discord: lobby command → RabbitMQ → faf-qai consumes and posts. It replaces two earlier ideas that were rejected for good reasons — a webhook in the client, which cannot verify who is asking, and an HTTP write endpoint on faf-qai, which adds inbound attack surface to a service that is not closely maintained.
Why this shape
- Identity is free. The client is already authenticated on the lobby socket. The server knows who is sending, so nothing has to trust a client-supplied player id.
- No new inbound surface. faf-qai consumes from the broker. It opens no port. This was the blocking objection to the HTTP variant.
- Kill switch. Unbind the queue and the feature is off — no client release, no code change, no restart.
- Durability. A qai restart does not lose requests; they wait in the queue.
Step 1 — Lobby server (this repo)
A command_request_replay_review handler on LobbyConnection, following the convention of command_invite_to_party and command_set_player_vetoes. It:
- Requires an authenticated connection (the existing
ensure_authenticated gate covers this — no new mechanism).
- Validates the payload against a fixed schema and rejects anything else.
- Stamps
player_id and login from the connection, not from the body. The client supplies content fields only. This is the property the whole design rests on; without it the impersonation hole would be back by another route.
- Rate limits per player and rejects over-limit requests before publishing, so abuse never reaches the bus.
- Publishes to
MQ_EXCHANGE_NAME with routing key request.replay_review.create.
Payload:
| field |
source |
notes |
player_id, login |
server |
from the connection |
replay_id |
client |
id only, never a url — see below |
map, game_mode, faction, rating, played_at |
client |
prefilled by the client, editable by the player |
goal |
client |
required, free text |
struggle |
client |
optional, free text |
requested_at |
server |
ISO 8601 UTC |
Two deliberate choices in that table:
Structured fields, not rendered Markdown. Formatting belongs at the destination, so changing how a review post reads does not need a client release, and a second consumer can use the same request differently.
A replay is named by id only. The client also knows how to name a replay by link or by a local file. Neither belongs on the bus: a link would let the client decide what a review post points at, and a file nobody else can fetch is not a review request. Those two cases keep using the client's existing copy-and-paste path.
Routing key follows the request.* / success.* convention already used by client_message_queue_service.py and avatar_change_queue_service.py.
Step 2 — Rate limit
There is no per-player rate limiter in the lobby server today, so this is new code. Proposal: one request per player per 24h, as a config constant next to the other tunables, held in memory in a small service.
In-memory means it does not survive a restart and each instance counts on its own — a spam guard rather than a quota. That looks like the right trade: the cost of a duplicate is a duplicate post, and the alternative is a schema migration plus a database write on a path that has no other reason to touch the database. Happy to be argued out of it.
Step 3 — Broker
A queue for faf-qai bound to request.replay_review.create on faf-rabbitmq. Ops task, not a code change.
Step 4 — faf-qai
A consumer on that queue that opens a forum post per request. DSharpPlus supports forum posts natively, so no REST workarounds. It renders the structured payload and writes the verified player name, so the author shown in Discord is one the server vouched for.
If the requesting player has linked their Discord account (AccountLinkService), a later version can mention them so the answer reaches them. Not in the first version.
Step 5 — Client
One frame alongside the existing lobby commands. The existing copy-and-paste path stays as the fallback for a player who is offline, for a replay that has no id, and for the case where any part of this is switched off — so nothing regresses if the feature is disabled.
Step 6 — Rollout
Ship the lobby side first with no consumer bound: the messages go nowhere and cost nothing. Bind qai's queue when both sides are deployed. The only hard ordering is lobby-before-client.
Decisions needed
- Routing key name — is
request.replay_review.create right?
- Rate limit — one per player per day, and is the in-memory spam guard acceptable, or does this want to be durable?
- Who creates the queue and binding?
- Is faf-qai's maintainer on board, and does the bot have permission to create posts in that channel? This one is outside both repos and it is what killed the Dostya attempt, so I would rather ask now than find out later.
A fifth, smaller one: the lobby currently sends nothing back on success and answers a rejection with the existing notice path, the same as command_invite_to_party. That means the client shows success optimistically. If an explicit acknowledgement is wanted instead, that is a new server→client command and worth deciding before clients are written against it.
Not in scope
The generic passthrough — a mechanism letting any client message reach the bus without a per-command change. Worth building, but it is the harder problem: it puts unvalidated client input on a bus whose producers are trusted precisely because it is internal only (see the docstring in client_message_queue_service.py). One command with a fixed routing key and a validated schema is the safe increment, and it is a concrete first candidate to migrate once the general mechanism exists.
Fan-out itself already works: the topic exchange lets any number of services bind their own queue to the same key, which ClientMessageQueueService and AvatarChangeQueueService already do side by side. So "multiple services consuming" needs nothing new.
What I am offering
I have written the lobby server side with tests, and the qai consumer. I need review and a deploy, not implementation time.
Goal in one sentence: a player presses "request replay review" in the client, and the request appears as a post in the training Discord's replay review forum channel, attributed to a FAF account the server vouched for.
This writes up Brutus5000's suggestion from Discord: lobby command → RabbitMQ → faf-qai consumes and posts. It replaces two earlier ideas that were rejected for good reasons — a webhook in the client, which cannot verify who is asking, and an HTTP write endpoint on faf-qai, which adds inbound attack surface to a service that is not closely maintained.
Why this shape
Step 1 — Lobby server (this repo)
A
command_request_replay_reviewhandler onLobbyConnection, following the convention ofcommand_invite_to_partyandcommand_set_player_vetoes. It:ensure_authenticatedgate covers this — no new mechanism).player_idandloginfrom the connection, not from the body. The client supplies content fields only. This is the property the whole design rests on; without it the impersonation hole would be back by another route.MQ_EXCHANGE_NAMEwith routing keyrequest.replay_review.create.Payload:
player_id,loginreplay_idmap,game_mode,faction,rating,played_atgoalstrugglerequested_atTwo deliberate choices in that table:
Structured fields, not rendered Markdown. Formatting belongs at the destination, so changing how a review post reads does not need a client release, and a second consumer can use the same request differently.
A replay is named by id only. The client also knows how to name a replay by link or by a local file. Neither belongs on the bus: a link would let the client decide what a review post points at, and a file nobody else can fetch is not a review request. Those two cases keep using the client's existing copy-and-paste path.
Routing key follows the
request.*/success.*convention already used byclient_message_queue_service.pyandavatar_change_queue_service.py.Step 2 — Rate limit
There is no per-player rate limiter in the lobby server today, so this is new code. Proposal: one request per player per 24h, as a config constant next to the other tunables, held in memory in a small service.
In-memory means it does not survive a restart and each instance counts on its own — a spam guard rather than a quota. That looks like the right trade: the cost of a duplicate is a duplicate post, and the alternative is a schema migration plus a database write on a path that has no other reason to touch the database. Happy to be argued out of it.
Step 3 — Broker
A queue for faf-qai bound to
request.replay_review.createonfaf-rabbitmq. Ops task, not a code change.Step 4 — faf-qai
A consumer on that queue that opens a forum post per request. DSharpPlus supports forum posts natively, so no REST workarounds. It renders the structured payload and writes the verified player name, so the author shown in Discord is one the server vouched for.
If the requesting player has linked their Discord account (
AccountLinkService), a later version can mention them so the answer reaches them. Not in the first version.Step 5 — Client
One frame alongside the existing lobby commands. The existing copy-and-paste path stays as the fallback for a player who is offline, for a replay that has no id, and for the case where any part of this is switched off — so nothing regresses if the feature is disabled.
Step 6 — Rollout
Ship the lobby side first with no consumer bound: the messages go nowhere and cost nothing. Bind qai's queue when both sides are deployed. The only hard ordering is lobby-before-client.
Decisions needed
request.replay_review.createright?A fifth, smaller one: the lobby currently sends nothing back on success and answers a rejection with the existing
noticepath, the same ascommand_invite_to_party. That means the client shows success optimistically. If an explicit acknowledgement is wanted instead, that is a new server→client command and worth deciding before clients are written against it.Not in scope
The generic passthrough — a mechanism letting any client message reach the bus without a per-command change. Worth building, but it is the harder problem: it puts unvalidated client input on a bus whose producers are trusted precisely because it is internal only (see the docstring in
client_message_queue_service.py). One command with a fixed routing key and a validated schema is the safe increment, and it is a concrete first candidate to migrate once the general mechanism exists.Fan-out itself already works: the topic exchange lets any number of services bind their own queue to the same key, which
ClientMessageQueueServiceandAvatarChangeQueueServicealready do side by side. So "multiple services consuming" needs nothing new.What I am offering
I have written the lobby server side with tests, and the qai consumer. I need review and a deploy, not implementation time.