Skip to content

feat: add Battle.net presence detection through BGS - #115

Merged
enimaloc merged 40 commits into
devfrom
feat/battlenet-bgs-presence
Nov 13, 2026
Merged

enimaloc merged 40 commits into
devfrom
feat/battlenet-bgs-presence

Conversation

@enimaloc

Copy link
Copy Markdown
Owner

Summary

Detect the Blizzard game a streamer is playing through Battle.net presence, the same way Minecraft works: Catapult bot accounts are friends with the streamer and read their presence over Battle.net's internal BGS protocol (pushed, no polling).

  • catapult-bgs (new module, protobuf-java only): TLS framing + RPC, session sequence (connect → ticket → logon → account subscribe → game-account select → friends sync → presence), friends list and actions, presence merging, reconnection with backoff, start watchdog, bounded TLS handshake.
  • catapult-api: bot pool (battlenet_service_account, battlenet_friend_link, migrations V108/V109 with @Version), headless Chromium login (Playwright) with the 2FA code entered in the admin and trusted cookies stored encrypted in the DB, one BGS session per bot only on the active site (battlenet.sessions.enabled + SiteRole guard), friend links driven by BGS events (bot invites the streamer and/or the streamer adds the bot), streamer link through Blizzard OAuth2, BattleNetGameGetter, admin API, battlenet.integration experiment.
  • catapult-web: Battle.net card on the channel page (live through channel events, owner only), bot pool section in the admin Keys tab (add, 2FA code, re-auth, edit, lift full, delete).
  • Docker: catapult-api runtime moves from Alpine to eclipse-temurin:25-jre-noble with Chromium installed at build time; catapult-web builder copies the new module's build file.

Modules to restart

catapult-api and catapult-web — deploy both together (shared DTO BattleNetAccountDto gained a field). New catapult-api image (noble base, Chromium baked in: larger image, check the pod memory limit).

Before enabling in production

  • Env: BATTLENET_ENABLED=true, BATTLENET_CLIENT_ID, BATTLENET_CLIENT_SECRET, and BATTLENET_SESSIONS_ENABLED=true only where the site guard runs (never on a second instance sharing the same bots).
  • Blizzard OAuth client: allow the redirect <web-url>/login/oauth2/code/battlenet.
  • Activate the battlenet.integration experiment in the admin (created as DRAFT).
  • Bot accounts need first name, last name and secret question on account.battle.net (checklist shown in the admin).

Verified

  • Unit/slice tests: catapult-bgs 79/79, catapult-common 409/409; catapult-api and catapult-web green except the Testcontainers/Redis/full-context classes the local sandbox cannot run (CI covers them, including the new access-matrix rows).
  • Live: real Battle.net spike (storageState login, minimal account subscribe, OAuth id == BGS account id), both Docker images build, local run on Postgres (V108/V109), bot login, streamer OAuth link, invitation accepted, live card, game detection.

Not verified live yet

  • Real 2FA code page and login-error selectors (generic selectors; three consecutive login timeouts stop the bot to avoid an account lock).
  • revokeInvitation, cross-region friendships, OnExternalChallenge.

Risk

Replaying Battle.net's client protocol with headless bot accounts goes against Blizzard's terms: bot accounts may be banned. The feature stays behind battlenet.enabled and the battlenet.integration experiment.

Java schemas are generated at build time from the bgs.desc descriptor set, with the
outer class names and Java packages patched where files would collide.
BgsClient runs the session sequence on an open BgsChannel: Connect, ticket
mint between Connect and Logon, Logon, minimal account subscribe, game
account selection, friends subscribe and paged sync, then the presence
watch once the self presence push arrives. The friends list and the
invitations survive across sessions and each resync reports the diff.
Friend actions fail at once outside a ready session and never change the
local state on their reply.
A sent invitation that vanished while offline is reported with
REASON_GONE_WHILE_OFFLINE (-1) instead of 0, which means accepted. A close
requested by the client reports CloseReason.LOCAL instead of IO.
BgsSession reads static fields through AccessFlag, which the English
conventions check accepts.
…detect games

Sessions open only with battlenet.sessions.enabled on the active site, close on standby and
reopen when the site becomes active. Presence pushes feed the Battle.net game getter; friend
events are republished off the session loop. Replaced sessions and sessions needing an admin
are logged and counted.
Version column on the service account; the login coordinator reloads and retries
its auth state and storage state writes once when an admin edit raced them.
Admin service and REST endpoints to enrol, re-authenticate, edit and delete the
Battle.net bot accounts; the BGS session restarts once an interactive login is ready.
Enrol streamers on the first ready bot with room and have it invite
them; move links with the bot's friend events (accept, reject, remove),
answer received invitations, mark a bot full on a friend cap failure
and announce every change as a battlenet.link.changed channel event.

Link lookups by BattleTag or account id now return lists, since nothing
makes them unique per bot.
…or full links

Send the accept/decline of a received invitation once the event's
transaction is committed, accept the invitation of a streamer whose link
was rejected, and move a link off a bot found full to the next bot with
room (deleting it when none is left).
…d OAuth

Secondary OAuth link on the signed-in Twitch user: the userinfo id and BattleTag
enroll the account with a bot, no token is kept. Adds the connect endpoints
(state, leave) and the Battle.net data on the channel dashboard.
Live-updated from BattleNetLinkChanged, with a case per link status, the OAuth connect
button, a disconnect action and mock data for the mock channel page.
Adds the bot pool section to the Keys tab: list, add, 2FA code entry, reauth,
enable/disable, lift full, change password and delete. The tab refreshes every 3 s
while a login is pending. Includes the mock store.
Polling only runs while the Keys tab is shown, including after an in-flight tick.
Each bot row can now rename the bot and change its fill order.
settings.gradle.kts includes catapult-bgs, so Gradle refuses to configure the build when its
directory is missing from the web builder stage.
…checks

The route, convention and access-matrix contexts now enable Battle.net, the matrix pins its user
and admin routes, and catapult-web spells each admin path in its call so the route check reads it.
…d ones

A re-mint asked while one is queued or running for the same bot now waits for that one, and a
queued login whose callers all gave up is dropped before Chromium opens. A BGS session that ends
while minting interrupts its mint, so an aborted attempt withdraws instead of logging in later.
…ssion

The handshake now has a 15 s read timeout, lifted once connected; the socket is closed on any
failure, connect included.
Headless Chromium writes its profile and crash data under $HOME, which a system user created
without a home lacks.
The Battle.net link keeps no token, so the authorized client Spring stored in memory after the
OAuth callback is removed at once.
The admin bot list now says whether a login waits for the 2FA code. A bot left CODE_REQUIRED by an
automatic login shows a hint to re-authenticate instead of a code field that could only answer
409, and the Keys tab stops polling for it.
Deleting a bot now disables the Battle.net getter of every streamer linked through it, as
unenrolling does, and tells their dashboards the link is gone once the bot is deleted.
…k controller

Controllers hold no repository (ControllerConventionsTest): the Battle.net link controller now reads
the caller like the other user routes.
@enimaloc
enimaloc merged commit 3c775c5 into dev Nov 13, 2026
5 checks passed
@enimaloc
enimaloc deleted the feat/battlenet-bgs-presence branch November 13, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant