diff --git a/config.default.json b/config.default.json index 3c65a39..70f9fcb 100644 --- a/config.default.json +++ b/config.default.json @@ -9,6 +9,8 @@ }, "loadDbcs": true, "hideGameMasters": true, + "hideBotAccounts": true, + "botAccountPatterns": "RNDBOT%", "transmogModule": false, "useZamCdn": false, "realms": [ diff --git a/docker-compose.mythica.yml b/docker-compose.mythica.yml index cfaef68..4153f7e 100644 --- a/docker-compose.mythica.yml +++ b/docker-compose.mythica.yml @@ -40,7 +40,19 @@ services: # --- behaviour ----------------------------------------------------- ACORE_ARMORY_LOAD_DBCS: ${ARMORY_LOAD_DBCS:-1} - ACORE_ARMORY_HIDE_GAME_MASTERS: ${ARMORY_HIDE_GMS:-1} + # Off by default here: GM accounts on this realm belong to real + # players who should appear in the armory like anyone else. Set to + # 1 to hide them from search and 404 their character pages. + ACORE_ARMORY_HIDE_GAME_MASTERS: ${ARMORY_HIDE_GMS:-0} + + # Playerbots are ordinary accounts, not game masters, so the GM + # filter does not touch them. Without this the armory lists ~1350 + # bots against ~29 real characters. + # Comma-separated LIKE patterns. RNDBOT% covers the Playerbots + # accounts; ahouse is the auction-house bot, which owns 9 mule + # characters. Add further service accounts here as they appear. + ACORE_ARMORY_HIDE_BOT_ACCOUNTS: ${ARMORY_HIDE_BOTS:-1} + ACORE_ARMORY_BOT_ACCOUNT_PATTERNS: ${ARMORY_BOT_PATTERNS:-RNDBOT%,ahouse} ACORE_ARMORY_TRANSMOG_MODULE: ${ARMORY_TRANSMOG:-0} # 0 = serve 3D model assets from the local data directory. diff --git a/docs/mythica-deploy.md b/docs/mythica-deploy.md index 299abc0..2b67642 100644 --- a/docs/mythica-deploy.md +++ b/docs/mythica-deploy.md @@ -129,5 +129,12 @@ FLUSH PRIVILEGES; page rather than shipping both. - **The Dockerfile builds from `node:16`**, which is end-of-life. It builds and runs fine; bumping it is a separate change from getting this deployed. -- **`hideGameMasters` defaults to on** here, so GM characters are hidden from - search and return 404. +- **`hideGameMasters` is off** in this stack, unlike upstream. GM accounts on + this realm belong to real players — `Spinnaker` among them — so hiding them + would hide people who should be listed. Set `ARMORY_HIDE_GMS=1` to restore + upstream behaviour. +- **Service accounts are hidden** via `hideBotAccounts` / `botAccountPatterns`, + a comma-separated list of LIKE patterns defaulting to `RNDBOT%,ahouse` — the + Playerbots accounts and the auction-house bot, which owns 9 mule characters. + None of them are game masters, so the GM filter never touched them. Add more + with `ARMORY_BOT_PATTERNS`, or set `ARMORY_HIDE_BOTS=0` to show them all. diff --git a/src/armory/Config.ts b/src/armory/Config.ts index 4ae6283..fc0bc80 100644 --- a/src/armory/Config.ts +++ b/src/armory/Config.ts @@ -31,6 +31,8 @@ export class Config { public iframeMode: IIframeModeConfig; public loadDbcs: boolean; public hideGameMasters: boolean; + public hideBotAccounts: boolean; + public botAccountPatterns: string; public transmogModule: boolean; public useZamCdn: boolean; public realms: IRealmConfig[]; diff --git a/src/armory/Utils.ts b/src/armory/Utils.ts index 366f5c9..6e0a062 100644 --- a/src/armory/Utils.ts +++ b/src/armory/Utils.ts @@ -60,4 +60,37 @@ export class Utils { background: obj.background.toString().padStart(padLength, "0"), }; } + + /** + * Quote a configuration-supplied string for inline use in SQL. + * + * The bot-account filter has to be spliced into join clauses that the query + * builder emits as raw SQL, so it cannot be passed as a bound parameter the + * way a normal value would be. The value comes from the operator's own + * config rather than from a request, but quoting it keeps a stray apostrophe + * from producing a broken or surprising query. + */ + public static quoteSqlString(value: string): string { + return `'${String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`; + } + + /** + * Build the LIKE test that identifies non-player accounts. + * + * `patterns` is a comma-separated list, so a realm can name several service + * accounts at once — Playerbots plus an auction-house bot, say. Returns an + * empty string when nothing is configured, which callers treat as "do not + * filter" rather than emitting an always-false clause. + */ + public static botAccountFilter(column: string, patterns: string): string { + const list = String(patterns ?? "") + .split(",") + .map((p) => p.trim()) + .filter((p) => p.length > 0); + + if (list.length === 0) { + return ""; + } + return `(${list.map((p) => `${column} LIKE ${Utils.quoteSqlString(p)}`).join(" OR ")})`; + } } diff --git a/src/armory/controllers/CharacterController.ts b/src/armory/controllers/CharacterController.ts index cbef2b8..1623502 100644 --- a/src/armory/controllers/CharacterController.ts +++ b/src/armory/controllers/CharacterController.ts @@ -342,16 +342,32 @@ export class CharacterController { private async getCharacterData(realm: IRealmConfig, character: string | number): Promise { const where = typeof character === "string" ? "LOWER(`characters`.`name`) = LOWER(?)" : "`characters`.`guid` = ?"; + + // Bot accounts are hidden the same way game masters are: left join the + // rows we do not want, then require the join to have missed. Keeping the + // pattern inline rather than bound avoids disturbing the positional + // values below. + const botFilter = this.armory.config.hideBotAccounts + ? Utils.botAccountFilter("`bot_account`.`username`", this.armory.config.botAccountPatterns) + : ""; + const botJoin = + botFilter === "" + ? "" + : `LEFT JOIN \`${realm.authDatabase}\`.\`account\` AS \`bot_account\` ON \`bot_account\`.\`id\` = \`characters\`.\`account\` AND ${botFilter}`; + const botWhere = botFilter === "" ? "" : "AND `bot_account`.`id` IS NULL"; + const [rows] = await this.armory.getCharactersDb(realm.name).query({ sql: ` - SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`race\`, \`class\`, \`gender\`, \`level\`, \`skin\`, \`face\`, \`hairStyle\`, \`hairColor\`, \`facialStyle\`, \`playerFlags\`, \`online\`, \`guild\`.\`name\` AS \`guild\` + SELECT \`characters\`.\`guid\`, \`characters\`.\`name\`, \`characters\`.\`race\`, \`characters\`.\`class\`, \`characters\`.\`gender\`, \`characters\`.\`level\`, \`characters\`.\`skin\`, \`characters\`.\`face\`, \`characters\`.\`hairStyle\`, \`characters\`.\`hairColor\`, \`characters\`.\`facialStyle\`, \`characters\`.\`playerFlags\`, \`characters\`.\`online\`, \`guild\`.\`name\` AS \`guild\` FROM \`characters\` LEFT JOIN \`guild_member\` ON \`guild_member\`.\`guid\` = \`characters\`.\`guid\` LEFT JOIN \`guild\` ON \`guild\`.\`guildid\` = \`guild_member\`.\`guildid\` LEFT JOIN \`${realm.authDatabase}\`.\`account_access\` ON \`account_access\`.\`id\` = \`characters\`.\`account\` AND \`account_access\`.\`RealmID\` IN (-1, ${realm.realmId}) AND \`account_access\`.\`gmlevel\` > 0 + ${botJoin} WHERE ${where} AND (\`account_access\`.\`id\` IS NULL OR ? = 0) + ${botWhere} `, values: [character, this.armory.config.hideGameMasters ? 1 : 0], timeout: this.armory.config.dbQueryTimeout, diff --git a/src/armory/controllers/GuildController.ts b/src/armory/controllers/GuildController.ts index c7eb65d..e7ca050 100644 --- a/src/armory/controllers/GuildController.ts +++ b/src/armory/controllers/GuildController.ts @@ -93,6 +93,22 @@ export class GuildController { ssp = ssp.where("`account_access`.`id` IS NULL"); } + const botFilter = this.armory.config.hideBotAccounts + ? Utils.botAccountFilter(`\`${realm.authDatabase}\`.\`account\`.\`username\``, this.armory.config.botAccountPatterns) + : ""; + if (botFilter !== "") { + ssp.joins.push({ + table1: "characters", + column1: "account", + table2: "account", + column2: "id", + database2: realm.authDatabase, + kind: "LEFT", + where: `AND ${botFilter}`, + }); + ssp = ssp.where(`\`${realm.authDatabase}\`.\`account\`.\`id\` IS NULL`); + } + const result = await ssp.where("`guildid` = ?", guildId).where("`deleteInfos_Account` IS NULL").run(this.armory.config.dbQueryTimeout); const ranks = await this.getGuildRanks(realm, guildId); diff --git a/src/armory/controllers/IndexController.ts b/src/armory/controllers/IndexController.ts index 0a72dd3..16c9052 100644 --- a/src/armory/controllers/IndexController.ts +++ b/src/armory/controllers/IndexController.ts @@ -55,6 +55,22 @@ export class IndexController { ssp = ssp.where("`account_access`.`id` IS NULL"); } + const botFilter = this.armory.config.hideBotAccounts + ? Utils.botAccountFilter(`\`${realm.authDatabase}\`.\`account\`.\`username\``, this.armory.config.botAccountPatterns) + : ""; + if (botFilter !== "") { + ssp.joins.push({ + table1: "characters", + column1: "account", + table2: "account", + column2: "id", + database2: realm.authDatabase, + kind: "LEFT", + where: `AND ${botFilter}`, + }); + ssp = ssp.where(`\`${realm.authDatabase}\`.\`account\`.\`id\` IS NULL`); + } + const result = await ssp.where("`deleteInfos_Account` IS NULL").run(this.armory.config.dbQueryTimeout); res.json({