Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
b252f9f
chore(release): v1.2.0 changelog
catapult-counterweight[bot] Nov 12, 2026
2df7052
chore(release): backport v1.2.0 to dev
catapult-counterweight[bot] Nov 12, 2026
0118885
feat(bgs): add the catapult-bgs module with the Battle.net service table
enimaloc Nov 13, 2026
483d06c
test(conventions): check the catapult-bgs sources are written in English
enimaloc Nov 13, 2026
72913f6
feat(bgs): add the BGS frame codec and RPC session
enimaloc Nov 13, 2026
2797536
fix(bgs): notify late close listeners and log swallowed session errors
enimaloc Nov 13, 2026
a5c7987
feat(bgs): merge presence deltas into per-friend game changes
enimaloc Nov 13, 2026
e89cd29
feat(bgs): add the BGS client session, friends list and presence watch
enimaloc Nov 13, 2026
f77c3b6
fix(bgs): flag invitations gone while offline and local closes
enimaloc Nov 13, 2026
f8de5ff
feat(bgs): reconnect BGS sessions with backoff and stop on replaced s…
enimaloc Nov 13, 2026
c61d642
fix(bgs): abort BGS session starts that never complete
enimaloc Nov 13, 2026
d680857
feat(battlenet): add the service account and friend link tables
enimaloc Nov 13, 2026
1345097
feat(battlenet): gate the integration and expose the link state
enimaloc Nov 13, 2026
bd47147
feat(battlenet): mint BGS tickets through a headless login with an ad…
enimaloc Nov 13, 2026
d4f37da
fix(battlenet): stop re-minting tickets after a rejected login
enimaloc Nov 13, 2026
cb5685b
feat(battlenet): drive the Battle.net login page with headless Chromium
enimaloc Nov 13, 2026
b9e9619
chore(docker): run catapult-api on a glibc base so Chromium can start
enimaloc Nov 13, 2026
d2db4bd
fix(battlenet): stop re-minting after repeated login timeouts
enimaloc Nov 13, 2026
36de5ed
fix(docker): keep Playwright from downloading browsers at runtime
enimaloc Nov 13, 2026
31c4c6a
feat(battlenet): keep one BGS session per bot on the active site and …
enimaloc Nov 13, 2026
f7f2d81
fix(battlenet): guard bot accounts against lost updates
enimaloc Nov 13, 2026
1ba9db2
feat(battlenet): manage the bot pool from the admin API
enimaloc Nov 13, 2026
e6fa264
fix(battlenet): handle concurrent bot edits and in-flight logins in t…
enimaloc Nov 13, 2026
d1df994
feat(battlenet): track friend links from BGS friend events
enimaloc Nov 13, 2026
95148c2
fix(battlenet): answer invitations after commit and recover rejected …
enimaloc Nov 13, 2026
cdba7e0
feat(battlenet): link a streamer's Battle.net account through Blizzar…
enimaloc Nov 13, 2026
4486e5a
feat(web): add the Battle.net connection card
enimaloc Nov 13, 2026
4b1b6b4
fix(web): keep Battle.net tags out of moderators' channel streams
enimaloc Nov 13, 2026
fb9983e
feat(web): manage Battle.net bots from the admin Keys tab
enimaloc Nov 13, 2026
6538ae8
fix(web): stop Battle.net polling off the Keys tab and edit bot labels
enimaloc Nov 13, 2026
550093a
fix(docker): copy the catapult-bgs build file into the web image builder
enimaloc Nov 13, 2026
140ad64
test(battlenet): cover the Battle.net routes in the route and access …
enimaloc Nov 13, 2026
8f08de4
fix(battlenet): run at most one ticket mint per bot and drop abandone…
enimaloc Nov 13, 2026
70b20ce
fix(bgs): bound the TLS handshake so a silent server cannot hang a se…
enimaloc Nov 13, 2026
5763fa0
fix(docker): give the catapult user a home directory for Chromium
enimaloc Nov 13, 2026
c3d6ffd
fix(battlenet): forget the Blizzard token once the account is linked
enimaloc Nov 13, 2026
d171bd7
fix(battlenet): only offer the code field while a login waits for it
enimaloc Nov 13, 2026
c8a67dc
fix(battlenet): notify streamers when their bot is deleted
enimaloc Nov 13, 2026
aa1549b
fix(battlenet): resolve the caller through ApiUserResolver in the lin…
enimaloc Nov 13, 2026
c78e520
fix(battlenet): declare the battlenet.integration experiment
enimaloc Nov 13, 2026
bd146c9
fix(battlenet): link through plain OAuth2 so the Catapult user servic…
enimaloc Nov 13, 2026
cd9643c
fix(web): style the Battle.net connect button like the other account …
enimaloc Nov 13, 2026
3c775c5
feat: merge Battle.net presence detection into dev
enimaloc Nov 13, 2026
05d822e
fix(api): show a bindings page whose bindings have no Twitch category
enimaloc Nov 13, 2026
a792ead
fix: merge the uncategorised bindings page fix into dev
enimaloc Nov 13, 2026
1ef74de
feat(mappings): store an IGDB game on game mappings and seed Blizzard…
enimaloc Nov 13, 2026
2a68225
feat(igdb): resolve detected games through their mapping's IGDB game
enimaloc Nov 13, 2026
4292c9c
feat(mappings): pick an IGDB game and deduce its Twitch category
enimaloc Nov 13, 2026
72eb335
test(mappings): cover the IGDB game search and its admin route
enimaloc Nov 13, 2026
dce5d3f
feat(web): pick an IGDB game for a game mapping
enimaloc Nov 13, 2026
0583773
fix(mappings): stop applying a cleared IGDB game's suggestions
enimaloc Nov 13, 2026
c4e972b
fix(mappings): reject a non-numeric IGDB game id
enimaloc Nov 13, 2026
cbd48bd
fix(web): explain the IGDB category deduction in mapping errors
enimaloc Nov 13, 2026
5ebb8db
test(perf): budget the game routes with their resolved IGDB and DTDD …
enimaloc Nov 13, 2026
1bdbc94
feat: merge IGDB games on game mappings into dev
enimaloc Nov 13, 2026
b24b1a8
refactor(minecraft): drop the service account label
enimaloc Nov 13, 2026
d91fdb9
refactor(battlenet): drop the service account label
enimaloc Nov 13, 2026
7507c4e
refactor: merge the service account label removal into dev
enimaloc Nov 13, 2026
792ed4b
chore(release): bump version to v1.3.0
catapult-counterweight[bot] Nov 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,85 @@ Toutes les versions publiées de Catapult, la plus récente en premier.

Ce fichier est régénéré par le bot à chaque release à partir des notes des [GitHub Releases](https://github.com/enimaloc/catapult/releases) : pour corriger une entrée, modifie les notes de la release (ou le changelog de la PR de release avant sa publication), pas ce fichier.

## [v1.2.0](https://github.com/enimaloc/catapult/releases/tag/v1.2.0) - 2026-11-12

### Features
- **api:** tell the settings whether the title's conditions and drops are offered (b1f56be)
- **api:** let an admin game mapping ignore its game by default (939704b)
- **web:** ignore a game by default from the admin game mappings (aac9d6e)
- **api:** expose each DTDD topic's severity in the public game detail (5f54d94)
- **api:** track each DTDD key's remaining quota and let bulk work keep a monthly reserve (0b11316)
- **api:** list the DTDD caches for the admins and refetch them under the rate limit (8ed75a3)
- **web:** add an admin tab for the DTDD caches with a refetch per entry and a full one (6f7012c)
- **api:** give the admins everything the DTDD caches keep about one entry (458d771)
- **web:** open a DTDD cache entry in full from the admin tab (525ecd0)
- **api:** keep each cached DTDD topic's severity and show it to the admins (67508d9)
- **web:** show each DTDD topic's severity in a wider entry dialog (0bb16dc)
- **api:** let a DTDD topic's severity keep its TW off below the mapping's least severe level (2092d4c)
- **api:** let the admins read and set each DTDD topic's least severe level on a TW (a3656a7)
- **web:** edit a TW's DTDD topics and their least severe level from the admin tab (9c099d8)
- **api:** tell a streamer why each of a binding's TWs is on (6e8c455)
- **web:** show a streamer why each of a binding's TWs is on, in a details dialog (2c1e5d9)
- **api:** run one TW recalculation at a time, follow and cancel it, waiting out Steam's rate limit (1ae844a)
- **web:** show the TW recalculation's progress in the admin tab, and let it be stopped (9f3102f)
- let an admin game mapping ignore its game by default (4dab0bc)
- an admin tab for the DTDD caches, with each topic's severity (4f681f3)
- let a DTDD topic's severity decide its TW, and tell streamers why each TW is on (45ffb48)
- follow the TW recalculation and respect Steam's rate limit (3a9d91d)
- **ratelimit:** add route keys and caller priority (cc3231d)
- **ratelimit:** learn limits and resume delays from 429s (cd059a6)
- **ratelimit:** persist learned rate limits (a62732a)
- **ratelimit:** throttle routes by learned limits with live priority (4465212)
- **ratelimit:** apply learned limits to outbound HTTP calls (6694be6)
- **ratelimit:** bound the routes per host and learn only from windows counted whole (e4227c5)
- **tw:** run the TW recalculation as bulk traffic under the learned rate limits (ca73b99)
- **admin:** expose the learned rate limits in the admin API (1f3f8db)
- **admin:** a tab to see, correct and reset the learned rate limits (e55fe64)
- merge learned rate limits into dev (47477c9)
- **keys:** count each shared key's calls against its window and daily or monthly limit (d37dd81)
- **admin:** merge the rate limits into the Keys tab, with each key's usage, updated live (d511999)
- merge the keys and rate limits tab into dev (831cad3)

### Fixes
- **api:** explain each title condition and the drops in the help, only when offered (d85167e)
- **web:** name the title's conditions and drop variables under its fields when offered (b5ad1b1)
- **api:** read DTDD's new topic severity instead of failing every item call (8478f18)
- **api:** leave the Steam store alone after a 429, for its Retry-After or a doubling wait (4bbbc96)
- **api:** resolve a Steam game's TWs again once the store's rate limit ends (d7170cb)
- **api:** decide the title help's parts in a service, not in the controller (859daae)
- show the title's conditions and drop variables in its help when offered (11e0f6e)
- **api:** read DTDD's new topic severity instead of failing every item call (63a3ae7)
- **web:** write the DTDD caches' API paths in their calls (025d804)
- **web:** stop completing an SSE stream the container already failed (434b489)
- **api:** leave the query string out of the http.client.requests uri tag (9029240)
- **api:** never take a Twitch website URL for a binding's category id (3a1bc1c)
- merge fix/1.1.1 into dev (142d52d)
- **xbox:** use a URI template for the store's product calls (8f4041a)
- **ratelimit:** fall back to the system clock in profiles without a Clock bean (4a7cd42)
- **ratelimit:** keep admin changes across instances and learn no tiny first limit (3add1c0)
- **steam:** let only live appdetails refusals stop the store, back off when nothing learned (1f6de35)
- **admin:** give a route's rate limit scope back to the learning (e83f87b)

### Refactoring
- **api:** inject the binding service's event publisher through its constructor (e13f736)
- **steam:** rely on learned rate limits for the store (da95d94)
- **ratelimit:** name the host row check hostScope so JSON keeps the host field (48cac62)

### Other
- merge dev into feat/dtdd-cache-admin (375f54a)
- merge dev into feat/tw-dtdd-severity (9ee1eef)
- **web:** cover the DTDD caches calls of the real and mock API services (bc3ce92)
- merge feat/dtdd-cache-admin into feat/tw-dtdd-severity (62bf3a7)
- **web:** cover the TW details and DTDD topics calls of the real and mock API services (6cf1834)
- merge dev into feat/dtdd-cache-admin (e6a9391)
- merge feat/dtdd-cache-admin into feat/tw-dtdd-severity (939f5f2)
- merge feat/tw-dtdd-severity into feat/tw-rebuild-progress (bc730cc)
- **common:** cover the admin DTOs' shorter constructors and older payloads (e9b65e8)
- **release:** promote dev straight to main, skipping beta (8ff3dc2)
- **release:** auto-merge the backports that bring a release back to dev (353eaaf)
- run the checks on pull requests only and cancel superseded runs (89bb860)
- merge ci-skip-beta into dev (0e7496d)

## [v1.1.0](https://github.com/enimaloc/catapult/releases/tag/v1.1.0) - 2026-11-10

### Features
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {

allprojects {
group = "fr.enimaloc"
version = "1.2.0"
version = "1.3.0"
}

subprojects {
Expand Down
20 changes: 16 additions & 4 deletions catapult-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,39 @@ RUN apk add --no-cache git
COPY gradlew settings.gradle.kts build.gradle.kts gradle.properties ./
COPY gradle ./gradle
COPY catapult-common/build.gradle.kts ./catapult-common/
COPY catapult-bgs/build.gradle.kts ./catapult-bgs/
COPY catapult-api/build.gradle.kts ./catapult-api/
COPY catapult-web/build.gradle.kts ./catapult-web/
RUN ./gradlew :catapult-api:dependencies --no-daemon -q

COPY catapult-common/src ./catapult-common/src
COPY catapult-bgs/src ./catapult-bgs/src
COPY catapult-api/src ./catapult-api/src
COPY CHANGELOG.md ./
COPY .git ./.git
# bootJar alone: `build` would also run the tests and leave a second (-plain) jar for the COPY below
RUN ./gradlew :catapult-api:bootJar --no-daemon

FROM eclipse-temurin:25-jre-alpine
# glibc base: Chromium (Battle.net login, Playwright) does not run on musl
FROM eclipse-temurin:25-jre-noble

WORKDIR /app

RUN apk add --no-cache curl jq
RUN addgroup -S catapult && adduser -S catapult -G catapult
RUN apt-get update && apt-get install -y --no-install-recommends curl jq && rm -rf /var/lib/apt/lists/*
RUN groupadd --system catapult && useradd --system --create-home --home-dir /home/catapult --gid catapult catapult

COPY --from=builder /app/catapult-api/build/libs/*.jar app.jar

RUN chown catapult:catapult app.jar
# Chromium and its OS libraries are installed at build time (never at startup) with Playwright's CLI, run from
# the boot jar's classpath through PropertiesLauncher; the browsers live in a folder the catapult user can read
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN java -Dloader.main=com.microsoft.playwright.CLI -cp app.jar \
org.springframework.boot.loader.launch.PropertiesLauncher install --with-deps chromium \
&& rm -rf /var/lib/apt/lists/* \
&& chown -R catapult:catapult /ms-playwright app.jar

# Only Chromium is baked in: Playwright.create() must not try to download the other browsers at runtime
ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1

USER catapult

Expand Down
4 changes: 3 additions & 1 deletion catapult-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ val springCloudContextVersion = "5.0.4"

dependencies {
implementation(project(":catapult-common"))
implementation(project(":catapult-bgs"))

implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
Expand Down Expand Up @@ -61,6 +62,7 @@ dependencies {
implementation("io.sentry:sentry-spring-boot-4-starter")
implementation("io.sentry:sentry-logback")
implementation("org.jsoup:jsoup:$jsoupVersion")
implementation("com.microsoft.playwright:playwright:1.64.0")
implementation("org.commonmark:commonmark:$commonmarkVersion")
implementation("org.springframework.cloud:spring-cloud-context:$springCloudContextVersion")

Expand Down Expand Up @@ -133,7 +135,7 @@ val englishConventions = tasks.register<Test>("englishConventions") {
testClassesDirs = testSourceSet.output.classesDirs
classpath = testSourceSet.runtimeClasspath
filter.includeTestsMatching("*EnglishConventionsTest")
listOf("catapult-common", "catapult-web").forEach { module ->
listOf("catapult-common", "catapult-bgs", "catapult-web").forEach { module ->
inputs.dir(rootProject.file("$module/src"))
.withPropertyName("${module}Sources")
.withPathSensitivity(PathSensitivity.RELATIVE)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package fr.enimaloc.catapult.api.admin;

import fr.enimaloc.catapult.api.ApiPaths;
import fr.enimaloc.catapult.service.battlenet.BattleNetServiceAccountAdmin;
import fr.enimaloc.catapult.service.battlenet.BattleNetServiceAccountAdmin.AccountDto;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping(ApiPaths.ADMIN + "/battlenet-accounts")
@RequiredArgsConstructor
@ConditionalOnBooleanProperty("battlenet.enabled")
public class ApiAdminBattleNetAccountsController {

private final BattleNetServiceAccountAdmin accounts;

@GetMapping
public List<AccountDto> list() {
return accounts.list();
}

/** 202: the account is stored and its login runs in the background (poll the list for its auth state). */
@PostMapping
public ResponseEntity<AccountDto> create(@RequestBody Map<String, String> body) {
return ResponseEntity.accepted().body(accounts.create(body.get("email"), body.get("password")));
}

/** The 2FA code of a login waiting for one; 409 when none is. */
@PostMapping("/{id}/code")
public ResponseEntity<Void> submitCode(@PathVariable UUID id, @RequestBody Map<String, String> body) {
accounts.submitCode(id, body.get("code"));
return ResponseEntity.accepted().build();
}

@PostMapping("/{id}/reauth")
public ResponseEntity<AccountDto> reauth(@PathVariable UUID id) {
return ResponseEntity.accepted().body(accounts.reauth(id));
}

@PatchMapping("/{id}")
public AccountDto update(@PathVariable UUID id, @RequestBody Map<String, Object> body) {
return accounts.update(id, body);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable UUID id) {
accounts.delete(id);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import fr.enimaloc.catapult.common.dto.admin.GameMappingDto;
import fr.enimaloc.catapult.common.dto.admin.GameMappingRequest;
import fr.enimaloc.catapult.common.dto.admin.GameMappingResult;
import fr.enimaloc.catapult.common.dto.admin.IgdbGameOption;
import fr.enimaloc.catapult.service.binding.AdminGameMappingService;
import fr.enimaloc.catapult.service.notification.AdminEventPublisher;
import fr.enimaloc.catapult.service.twitch.TwitchCategory;
Expand Down Expand Up @@ -86,4 +87,11 @@ public List<TwitchCategory> categories(@RequestParam(required = false) String q)
if (q == null || q.isBlank()) return List.of();
return categoryService.searchCategories(q.trim());
}

/** The IGDB game search of the form (at most 10); empty for a blank query or when IGDB is unavailable. */
@GetMapping("/igdb-games")
public List<IgdbGameOption> igdbGames(@RequestParam(required = false) String q) {
if (q == null || q.isBlank()) return List.of();
return service.searchIgdbGames(q.trim());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public ResponseEntity<?> startDeviceCode() {
/** 202 while the admin hasn't validated the code on microsoft.com/link; 200 + the created account afterwards. */
@PostMapping
public ResponseEntity<?> create(@RequestBody Map<String, String> body) {
return accounts.create(body.get("deviceCode"), body.getOrDefault("label", "Minecraft account"))
return accounts.create(body.get("deviceCode"))
.<ResponseEntity<?>>map(ResponseEntity::ok)
.orElseGet(ApiAdminMinecraftAccountsController::pending);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import fr.enimaloc.catapult.common.dto.channel.DtddMappingProposalDto;
import fr.enimaloc.catapult.common.dto.channel.DtddMappingStatusDto;
import fr.enimaloc.catapult.common.dto.channel.GameDto;
import fr.enimaloc.catapult.common.dto.channel.BattleNetData;
import fr.enimaloc.catapult.common.dto.channel.MinecraftData;
import fr.enimaloc.catapult.common.dto.channel.ObsData;
import fr.enimaloc.catapult.common.dto.channel.TwitchatData;
Expand Down Expand Up @@ -51,6 +52,7 @@
import fr.enimaloc.catapult.service.igdb.AdminCclService;
import fr.enimaloc.catapult.api.CustomBindingViews;
import fr.enimaloc.catapult.service.igdb.IgdbService;
import fr.enimaloc.catapult.service.battlenet.BattleNetLinkStateService;
import fr.enimaloc.catapult.service.minecraft.MinecraftLinkStateService;
import fr.enimaloc.catapult.service.notification.TwitchatWidgetSettingsService;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -92,6 +94,8 @@ public class ChannelDashboardAssembler {
private final OAuthTokenRepository oAuthTokenRepository;
// Only present when minecraft.enabled (the service is @ConditionalOnBooleanProperty).
private final Optional<MinecraftLinkStateService> minecraftLinkStates;
// Only present when battlenet.enabled.
private final Optional<BattleNetLinkStateService> battleNetLinkStates;
private final TwitchatWidgetSettingsService twitchatWidgetSettingsService;
private final RaidRestoreGateService raidRestoreGate;
private final CustomBindingViews customBindingViews;
Expand Down Expand Up @@ -147,6 +151,7 @@ public ChannelPageData page(UserAccount viewer, UserAccount channel, String user
steamData(channel, owner),
xboxEnabled ? new XboxData(hasXboxToken(channel)) : null,
owner ? minecraftData(channel) : null,
owner ? battleNetData(channel) : null,
owner ? obsData(widget) : null,
owner ? TwitchatData.of(widget.getTwitchatBranch()) : null,
custom,
Expand Down Expand Up @@ -199,7 +204,7 @@ private PagedBindings bindings(UserAccount channel, int page, String status, Str
}
Map<String, String> boxArts = boxArts(bindings.getContent());
List<BindingDto> content = bindings.getContent().stream()
.map(binding -> toBindingDto(binding, boxArts.get(binding.getTwitchGameId())))
.map(binding -> toBindingDto(binding, boxArt(boxArts, binding.getTwitchGameId())))
.toList();
return new PagedBindings(bindings.getNumber(), bindings.getTotalPages(), bindings.getTotalElements(), content);
}
Expand All @@ -214,6 +219,11 @@ private Map<String, String> boxArts(List<GameBinding> bindings) {
.collect(Collectors.toMap(TwitchCategoryCache::getId, TwitchCategoryCache::getBoxArtUrl));
}

/** A category's box art, absent for a binding with no Twitch category (an immutable map rejects a null key). */
private static String boxArt(Map<String, String> boxArts, String twitchGameId) {
return twitchGameId == null ? null : boxArts.get(twitchGameId);
}

private static BindingDto toBindingDto(GameBinding binding, String boxArtUrl) {
return new BindingDto(binding.getId().toString(), binding.getStatus().name(), binding.getSourceType().name(),
binding.getSourceName(), binding.getTwitchGameId(), binding.getTwitchGameName(), binding.isIgnored(),
Expand Down Expand Up @@ -249,6 +259,15 @@ private MinecraftData minecraftData(UserAccount channel) {
.orElse(null);
}

/** The Battle.net link as the connect endpoint states it, null without Battle.net or when it's unavailable. */
private BattleNetData battleNetData(UserAccount channel) {
return battleNetLinkStates
.map(service -> service.state(channel))
.filter(state -> !BattleNetLinkStateService.UNAVAILABLE.equals(state.status()))
.map(state -> new BattleNetData(state.status(), state.battleTag(), state.serviceAccountBattleTag()))
.orElse(null);
}

/** The Twitchat relay's OBS settings, without the password itself. */
private static ObsData obsData(TwitchatWidgetSettings settings) {
return new ObsData(settings.isEnabled(), settings.getObsHost(), settings.getObsPort(),
Expand Down Expand Up @@ -331,11 +350,9 @@ public DtddMappingStatusDto dtddMapping(UserAccount viewer, UserAccount channel,
return new DtddMappingStatusDto(current, pending, canValidate, id, viewerIsAdmin);
}

/** IGDB entry of a detected game: by app id for Steam games, by name otherwise. */
/** IGDB entry of a detected game, as {@link IgdbService#resolveDetected} resolves it. */
private Optional<IgdbService.IgdbGame> igdbGame(DetectedGame game) {
return game.getSourceType() == GameBinding.SourceType.STEAM
? igdbService.findBySteamAppId(game.getSourceId())
: igdbService.findByName(game.getSourceName());
return igdbService.resolveDetected(game.getSourceType(), game.getSourceId(), game.getSourceName());
}

private String dtddName(Long dtddId) {
Expand Down Expand Up @@ -369,7 +386,7 @@ public Optional<BindingDto> currentBinding(UserAccount channel) {
return gameStateService.getLastKnownGame(channel)
.flatMap(game -> gameBindingRepository
.findByUserAndSourceIdAndSourceType(channel, game.getSourceId(), game.getSourceType()))
.map(binding -> toBindingDto(binding, boxArts(List.of(binding)).get(binding.getTwitchGameId())));
.map(binding -> toBindingDto(binding, boxArt(boxArts(List.of(binding)), binding.getTwitchGameId())));
}

/** The detected game, with the id of the channel's binding for it when there is one. */
Expand Down
Loading
Loading