diff --git a/WEBSITE_INTEGRATION.md b/WEBSITE_INTEGRATION.md index 8f45fe9bbe..ce985e37a1 100644 --- a/WEBSITE_INTEGRATION.md +++ b/WEBSITE_INTEGRATION.md @@ -2,6 +2,55 @@ This document describes how the TI4 Map Generator Bot integrates with the AsyncTI4 website UI. +## Game Webhooks + +Trusted external integrations can subscribe to non-FoW game changes without polling. Server operators create `webhook_user` rows with the callback URL and SHA-256 API key hash. + +Supported events: + +- `TURN_CHANGED` +- `PHASE_CHANGED` + +Endpoints: + +- `GET /api/public/game/webhook/eventTypes` returns supported event names, trigger descriptions, delivery settings, and payload fields. +- `PUT /api/public/game/{gameName}/webhook` with `X-API-Key` and JSON body: + +```json +{ "eventTypes": ["TURN_CHANGED", "PHASE_CHANGED"] } +``` + +- `DELETE /api/public/game/{gameName}/webhook` with `X-API-Key` + +Example `TURN_CHANGED` payload: + +```json +{ + "gameName": "pbd1234", + "eventType": "TURN_CHANGED", + "phaseOfGame": "action", + "round": 5, + "activePlayerId": "123456789012345678", + "activeFaction": "arborec", + "timestamp": "2026-06-02T14:30:00Z" +} +``` + +Example `PHASE_CHANGED` payload: + +```json +{ + "gameName": "pbd1234", + "eventType": "PHASE_CHANGED", + "previousPhaseOfGame": "strategy", + "phaseOfGame": "agenda", + "round": 4, + "timestamp": "2026-06-02T14:30:00Z" +} +``` + +Delivery is asynchronous. Webhook failures never block game flow; transient failures may be retried once, and permanent 4xx failures are not retried. + ## Image Asset Pipeline Game assets (faction banners, planet images, system tiles, etc.) are automatically uploaded to a CloudFront-backed S3 bucket and served to the website. diff --git a/src/main/java/ti4/discord/interactions/buttons/handlers/agenda/AgendaResolveButtonHandler.java b/src/main/java/ti4/discord/interactions/buttons/handlers/agenda/AgendaResolveButtonHandler.java index 121dea25f4..4e22de6222 100644 --- a/src/main/java/ti4/discord/interactions/buttons/handlers/agenda/AgendaResolveButtonHandler.java +++ b/src/main/java/ti4/discord/interactions/buttons/handlers/agenda/AgendaResolveButtonHandler.java @@ -70,6 +70,7 @@ import ti4.service.info.SecretObjectiveInfoService; import ti4.service.leader.CommanderUnlockCheckService; import ti4.service.turn.StartTurnService; +import ti4.service.webhook.GameWebhookNotifierFacade; @UtilityClass class AgendaResolveButtonHandler { @@ -467,10 +468,13 @@ private static void sendNextStepUi( + ", please use this buttons to proceed after fully resolving the agenda."; buttons = StartTurnService.getStartOfTurnButtons(executiveOrderPlayer, game, true, event); game.removeStoredValue("executiveOrder"); + String previousPhaseOfGame = game.getPhaseOfGame(); game.updateActivePlayer(executiveOrderPlayer); Player oldSpeaker = game.getPlayer(game.getStoredValue("oldSpeakerExecutiveOrder")); game.setSpeaker(oldSpeaker); game.setPhaseOfGame("action"); + GameWebhookNotifierFacade.phaseChanged(game, previousPhaseOfGame, "action"); + GameWebhookNotifierFacade.turnChanged(game); MessageHelper.sendMessageToChannelWithButtons(event.getChannel(), voteMessage, buttons); } if (!"action".equalsIgnoreCase(game.getPhaseOfGame())) { diff --git a/src/main/java/ti4/discord/interactions/buttons/handlers/agenda/ChecksAndBalancesButtonHandler.java b/src/main/java/ti4/discord/interactions/buttons/handlers/agenda/ChecksAndBalancesButtonHandler.java index ea245c94f6..abe8034bb3 100644 --- a/src/main/java/ti4/discord/interactions/buttons/handlers/agenda/ChecksAndBalancesButtonHandler.java +++ b/src/main/java/ti4/discord/interactions/buttons/handlers/agenda/ChecksAndBalancesButtonHandler.java @@ -15,6 +15,7 @@ import ti4.message.MessageHelper; import ti4.service.game.StartPhaseService; import ti4.service.player.PlayerStatsService; +import ti4.service.webhook.GameWebhookNotifierFacade; @UtilityClass class ChecksAndBalancesButtonHandler { @@ -65,8 +66,10 @@ public static void resolvePt2ChecksNBalances( if (privatePlayer == null) { privatePlayer = game.getRealPlayers().getFirst(); } + String previousPhaseOfGame = game.getPhaseOfGame(); game.setPhaseOfGame("strategy"); game.updateActivePlayer(privatePlayer); + GameWebhookNotifierFacade.phaseChanged(game, previousPhaseOfGame, "strategy"); MessageHelper.sendMessageToChannelWithButtons( privatePlayer.getCorrectChannel(), privatePlayer.getRepresentationUnfogged() diff --git a/src/main/java/ti4/discord/interactions/buttons/handlers/edict/EdictPhaseHandler.java b/src/main/java/ti4/discord/interactions/buttons/handlers/edict/EdictPhaseHandler.java index a0dd23222c..80d23ab0b3 100644 --- a/src/main/java/ti4/discord/interactions/buttons/handlers/edict/EdictPhaseHandler.java +++ b/src/main/java/ti4/discord/interactions/buttons/handlers/edict/EdictPhaseHandler.java @@ -15,6 +15,7 @@ import ti4.message.MessageHelper; import ti4.model.AgendaModel; import ti4.model.DeckModel; +import ti4.service.webhook.GameWebhookNotifierFacade; @UtilityClass public class EdictPhaseHandler { @@ -30,7 +31,9 @@ public List getEdictDeck(Game game) { @ButtonHandler("edictPhase") public static void edictPhase(GenericInteractionCreateEvent event, Game game) { + String previousPhaseOfGame = game.getPhaseOfGame(); game.setPhaseOfGame("agenda"); + GameWebhookNotifierFacade.phaseChanged(game, previousPhaseOfGame, "agenda"); List edicts = getEdictDeck(game); List