Compare commits

...
Sign in to create a new pull request.

8 commits

Author SHA1 Message Date
92fda8086e Merge pull request 'Filter the auction-house bot as well as Playerbots' (#4) from filter-service-accounts into master
Some checks failed
Build / build (push) Has been cancelled
Lint / eslint (push) Has been cancelled
Reviewed-on: #4
2026-09-02 15:42:06 -06:00
Claude
28b9fd1aee
Filter several service accounts, not just Playerbots
Some checks failed
Build / build (push) Has been cancelled
Lint / eslint (push) Has been cancelled
Build / build (pull_request) Has been cancelled
Lint / eslint (pull_request) Has been cancelled
The auction-house bot account (ahouse) owns 9 mule characters that were
showing up in the armory alongside real players. One LIKE pattern could not
cover both it and RNDBOT%.

Replaces botAccountPattern with botAccountPatterns, a comma-separated list
of LIKE patterns, and builds the clause with Utils.botAccountFilter. An empty
list now disables the filter outright rather than emitting a clause that
matches nothing, so misconfiguration fails open instead of hiding everyone.

The stack default becomes "RNDBOT%,ahouse"; config.default.json keeps the
generic "RNDBOT%" so the fork stays upstreamable.

Verified against the live realm: 29 listed before, 20 after. The 9 excluded
are the ahouse mules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NW2ooBP2KPqQVzdZZDMvZd
2026-09-02 21:41:06 +00:00
24c7c403ba Merge pull request 'Fix ambiguous 'online' column breaking every character page' (#3) from fix-ambiguous-online into master
Some checks are pending
Build / build (push) Waiting to run
Lint / eslint (push) Waiting to run
Reviewed-on: #3
2026-09-02 15:12:08 -06:00
Claude
0e7cf0f988
Fix ambiguous 'online' column on character pages
Some checks failed
Build / build (push) Waiting to run
Lint / eslint (push) Waiting to run
Build / build (pull_request) Has been cancelled
Lint / eslint (pull_request) Has been cancelled
The bot-account filter joins acore_auth.account, which has its own `online`
column. The character query selected several columns unqualified, so `online`
became ambiguous and every character page returned a 500:

  Error: Column 'online' in field list is ambiguous
    at CharacterController.getCharacterData

Qualifies every column in that SELECT with `characters`, rather than only the
one that collided, so a future join cannot reintroduce this. The search and
guild listings were never affected: DataTablesSsp always qualifies its columns
with the base table, and only this hand-written query did not.

Verified against the live realm - the previously failing query for
Heavenlymagi now returns its row, and a bot name still returns none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NW2ooBP2KPqQVzdZZDMvZd
2026-09-02 21:10:41 +00:00
424d65233b Merge pull request 'Hide Playerbot accounts from search, character pages and guild rosters' (#2) from hide-bot-accounts into master
Some checks are pending
Build / build (push) Waiting to run
Lint / eslint (push) Waiting to run
Reviewed-on: #2
2026-09-02 14:21:49 -06:00
Claude
6d7398d055
Show game master characters in the armory
Some checks failed
Build / build (push) Has been cancelled
Lint / eslint (push) Has been cancelled
Build / build (pull_request) Has been cancelled
Lint / eslint (pull_request) Has been cancelled
Upstream hides GM accounts by default, which assumes GM means staff-only.
On this realm GM accounts belong to real players — Spinnaker, the highest
level character on the server, is gmlevel 3 — so the filter was hiding
people who should be listed.

Flips the stack default to 0 and documents both this and the bot filter.
ARMORY_HIDE_GMS=1 restores upstream behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NW2ooBP2KPqQVzdZZDMvZd
2026-09-02 20:10:54 +00:00
Claude
cc7ca06182
Hide Playerbot accounts from search, character pages and guild rosters
Some checks failed
Build / build (push) Waiting to run
Lint / eslint (push) Waiting to run
Build / build (pull_request) Has been cancelled
Lint / eslint (pull_request) Has been cancelled
Playerbots are ordinary accounts, not game masters, so hideGameMasters does
not touch them. On a realm running mod-playerbots that means the armory is
almost entirely bots: 1,377 characters listed, of which about 1,350 are bots.

Adds two options, hideBotAccounts (default true) and botAccountPattern
(default "RNDBOT%"), applied the same way the game master filter already
works: left join the accounts we do not want, then require the join to have
missed. A realm without bots matches nothing, so the default is harmless.

Applied in the search listing, guild rosters, and the character lookup, so a
bot's page 404s rather than rendering.

The pattern is spliced into join clauses the query builder emits as raw SQL,
so it cannot be a bound parameter. It comes from the operator's own config
rather than from a request, but Utils.quoteSqlString quotes it so a stray
apostrophe cannot produce a broken query.

Verified against the live realm:

  listed before  1377
  listed after     27
  Spinnaker, Kdog  shown
  Rarzosh (bot)    hidden

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NW2ooBP2KPqQVzdZZDMvZd
2026-09-02 20:08:24 +00:00
ffdeacc0c4 Merge pull request #1: Portainer stack and deployment doc for Nox
Some checks are pending
Build / build (push) Waiting to run
Lint / eslint (push) Waiting to run
2026-09-02 13:10:56 -06:00
8 changed files with 108 additions and 4 deletions

View file

@ -9,6 +9,8 @@
},
"loadDbcs": true,
"hideGameMasters": true,
"hideBotAccounts": true,
"botAccountPatterns": "RNDBOT%",
"transmogModule": false,
"useZamCdn": false,
"realms": [

View file

@ -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.

View file

@ -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.

View file

@ -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[];

View file

@ -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 ")})`;
}
}

View file

@ -342,16 +342,32 @@ export class CharacterController {
private async getCharacterData(realm: IRealmConfig, character: string | number): Promise<ICharacterData> {
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,

View file

@ -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);

View file

@ -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({