diff --git a/.gitignore b/.gitignore index 75454837d..a5f00f30e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules package-lock.json .env +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/package.json b/package.json index 399aa5fb7..bd8cc4066 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,16 @@ "license": "MIT", "dependencies": { "discord.js": "^14.9.0", + "escape-html": "^1.0.3", "fuzzysort": "^2.0.4", "lodash.startcase": "^4.4.0", "markdown-escape": "^2.0.0", "node-fetch": "^3.3.1" }, "devDependencies": { + "@playwright/test": "^1.34.2", "@total-typescript/ts-reset": "^0.4.2", + "@types/escape-html": "^1.0.2", "@types/lodash.startcase": "^4.4.7", "@types/markdown-escape": "^1.1.0", "@types/node": "^20.2.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..8d244eec3 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,13 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { trace: 'on-first-retry' }, + + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], +}); diff --git a/src/commands/stats.ts b/src/commands/stats.ts index da2569cdc..109006d8e 100644 --- a/src/commands/stats.ts +++ b/src/commands/stats.ts @@ -1,25 +1,35 @@ -import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'; -import markdownEscape from 'markdown-escape'; -import { INFO_COLOR } from '../constants/colors.js'; +import { SlashCommandBuilder } from 'discord.js'; +import escapeHTML from 'escape-html'; +import GenericStats from '../components/GenericStats/index.js'; +import Screenshot from '../components/Screenshot.js'; +import TitleBar from '../components/TitleBar.js'; +import Wrapper from '../components/Wrapper.js'; +import { BLITZ_SERVERS } from '../constants/servers.js'; import usernameAutocomplete from '../core/autocomplete/username.js'; import getBlitzAccount from '../core/blitz/getBlitzAccount.js'; import getWargamingResponse from '../core/blitz/getWargamingResponse.js'; -import blitzStarsLinks from '../core/blitzstars/blitzStarsLinks.js'; import getBlitzStarsAccount from '../core/blitzstars/getBlitzStarsAccount.js'; -import poweredByBlitzStars from '../core/blitzstars/poweredByBlitzStars.js'; -import cleanTable from '../core/interaction/cleanTable.js'; import cmdName from '../core/interaction/cmdName.js'; import addUsernameOption from '../core/options/addUsernameOption.js'; import { args } from '../core/process/args.js'; import { CommandRegistry } from '../events/interactionCreate.js'; +import { browser } from '../index.js'; import { AccountInfo, AllStats } from '../types/accountInfo.js'; +import { PlayerClanData } from '../types/playerClanData.js'; import { BlitzStartsComputedPeriodicStatistics } from '../types/statistics.js'; type Period = 'today' | '30' | '90' | 'career'; +const periodNames: Record = { + today: "Today's statistics", + 30: '30-day statistics', + 90: '90-day statistics', + career: 'Career statistics', +}; + export default { inProduction: true, - inDevelopment: false, + inDevelopment: true, inPublic: true, command: new SlashCommandBuilder() @@ -50,6 +60,10 @@ export default { `https://api.wotblitz.${server}/wotb/account/info/?application_id=${args['wargaming-application-id']}&account_id=${id}`, ); if (!accountInfo) return; + const clanData = await getWargamingResponse( + `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${args['wargaming-application-id']}&account_id=${id}&extra=clan`, + ); + if (!clanData) return; const blitzStarsAccount = await getBlitzStarsAccount(interaction, id); if (!blitzStarsAccount) return; @@ -97,98 +111,76 @@ export default { ? blitzStarsAccount.statistics : blitzStarsAccount[`period${period}d`]; } - if (!stats!) return; + if (!stats) return; - await interaction.editReply({ - embeds: [ - poweredByBlitzStars( - new EmbedBuilder() - .setColor(INFO_COLOR) - .setTitle( - `${markdownEscape(blitzStarsAccount.nickname)}'s ${ - period === 'career' - ? period - : `${period === 'today' ? 'today' : `${period} day`}` - } stats`, - ) - .setDescription( - stats.all.battles > 0 - ? `${ - period === 'today' - ? '**⚠ "Today" stats are in development. Some stats are not available/wrong.**\n\n' - : '' - }${cleanTable([ - [ - 'Winrate', - `${(100 * (stats.all.wins / stats.all.battles)).toFixed( - 2, - )}%`, - ], - [ - 'Survival', - `${( - 100 * - (stats.all.survived_battles / stats.all.battles) - ).toFixed(2)}%`, - ], - [ - 'Accuracy', - `${((stats.all.hits / stats.all.shots) * 100).toFixed( - 2, - )}%`, - ], - ['WN8', stats.wn8.toFixed(0)], - ['WN7', stats.wn7.toFixed(0)], - [], - ['Battles', stats.all.battles], - ['Wins', stats.all.wins], - ['Losses', stats.all.losses], - ['Average tier', stats.avg_tier.toFixed(2)], - [], - [ - 'Shots per battle', - (stats.all.shots / stats.all.battles).toFixed(2), - ], - [ - 'Hits per battle', - (stats.all.hits / stats.all.battles).toFixed(2), - ], - [ - 'Damage per battle', - (stats.all.damage_dealt / stats.all.battles).toFixed(0), - ], - [ - 'Kills per battle', - (stats.all.frags / stats.all.battles).toFixed(2), - ], - [ - 'Spots per battle', - (stats.all.spotted / stats.all.battles).toFixed(2), - ], - [ - 'XP per battle', - (stats.all.xp / stats.all.battles).toFixed(0), - ], - [], - [ - 'Damage ratio', - ( - stats.all.damage_dealt / stats.all.damage_received - ).toFixed(2), - ], - [ - 'Kills to death ratio', - ( - stats.all.frags / - (stats.all.battles - stats.all.survived_battles) - ).toFixed(2), - ], - ])}\n\n${blitzStarsLinks(server, blitzStarsAccount.nickname)}` - : 'No battles played in this period.', - ), + const page = await browser.newPage(); + await page.setViewportSize({ width: 640, height: 1 }); + await page.setContent( + Screenshot( + Wrapper( + TitleBar( + escapeHTML(blitzStarsAccount.nickname), + clanData[id]?.clan + ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png` + : 'https://i.imgur.com/uq6pKbh.png', + clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : '', + `${periodNames[period]} • ${new Date().toDateString()} • ${ + BLITZ_SERVERS[server] + }`, + ), + GenericStats([ + [ + 'Winrate', + `${(100 * (stats.all.wins / stats.all.battles)).toFixed(2)}%`, + ], + ['WN8', stats.wn8.toFixed(0)], + [ + 'Survival', + `${( + 100 * + (stats.all.survived_battles / stats.all.battles) + ).toFixed(2)}%`, + ], + [ + 'Accuracy', + `${((stats.all.hits / stats.all.shots) * 100).toFixed(2)}%`, + ], + ['Battles', stats.all.battles], + ['Wins', stats.all.wins], + ['Losses', stats.all.losses], + [ + 'Average damage', + (stats.all.damage_dealt / stats.all.battles).toFixed(0), + ], + ['Average XP', (stats.all.xp / stats.all.battles).toFixed(0)], + ['Average shots', (stats.all.shots / stats.all.battles).toFixed(2)], + ['Average hits', (stats.all.hits / stats.all.battles).toFixed(2)], + ['Average kills', (stats.all.frags / stats.all.battles).toFixed(2)], + [ + 'Average spots', + (stats.all.spotted / stats.all.battles).toFixed(2), + ], + ['Average tier', stats.avg_tier.toFixed(2)], + [ + 'Damage ratio', + (stats.all.damage_dealt / stats.all.damage_received).toFixed(2), + ], + [ + 'Kills to death ratio', + ( + stats.all.frags / + (stats.all.battles - stats.all.survived_battles) + ).toFixed(2), + ], + ]), ), - ], - }); + ), + ); + const screenshot = await page + .locator('#screenshot') + .screenshot({ omitBackground: true, scale: 'css' }); + await page.close(); + await interaction.editReply({ files: [screenshot] }); console.log(`Showing stats for ${blitzStarsAccount.nickname}`); }, diff --git a/src/commands/today.ts b/src/commands/today.ts deleted file mode 100644 index 7b3611d39..000000000 --- a/src/commands/today.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { SlashCommandBuilder } from 'discord.js'; -import usernameAutocomplete from '../core/autocomplete/username.js'; -import cmdName from '../core/interaction/cmdName.js'; -import addUsernameOption from '../core/options/addUsernameOption.js'; -import { CommandRegistry } from '../events/interactionCreate.js'; - -export default { - inProduction: false, - inDevelopment: true, - inPublic: true, - - command: new SlashCommandBuilder() - .setName(cmdName('today')) - .setDescription('A brief summary of your performance today') - .addStringOption(addUsernameOption), - - async execute(interaction) { - interaction.editReply("let's do this!"); - }, - - autocomplete: usernameAutocomplete, -} satisfies CommandRegistry; diff --git a/src/components/Children.ts b/src/components/Children.ts new file mode 100644 index 000000000..972309359 --- /dev/null +++ b/src/components/Children.ts @@ -0,0 +1,5 @@ +export type ChildrenArgument = string | string[]; + +export default function Children(children: string[]) { + return children.join('\n'); +} diff --git a/src/components/GenericStats/components/GenericStatsColumn.ts b/src/components/GenericStats/components/GenericStatsColumn.ts new file mode 100644 index 000000000..d40ff797f --- /dev/null +++ b/src/components/GenericStats/components/GenericStatsColumn.ts @@ -0,0 +1,19 @@ +import { Stat } from '../index.js'; +import GenericStatsRow from './GenericStatsRow.js'; + +export default function GenericStatsColumn(stats: Stat[]) { + return ` + + +
+ ${stats.map((stat) => GenericStatsRow(stat)).join('\n')} +
+ `; +} diff --git a/src/components/GenericStats/components/GenericStatsRow.ts b/src/components/GenericStats/components/GenericStatsRow.ts new file mode 100644 index 000000000..cc647d360 --- /dev/null +++ b/src/components/GenericStats/components/GenericStatsRow.ts @@ -0,0 +1,29 @@ +import { Stat } from '../index.js'; + +export default function GenericStatsRow(stat: Stat) { + return ` + + +
+ ${stat[0]} + ${stat[1]} +
+ `; +} diff --git a/src/components/GenericStats/index.ts b/src/components/GenericStats/index.ts new file mode 100644 index 000000000..34560a40c --- /dev/null +++ b/src/components/GenericStats/index.ts @@ -0,0 +1,27 @@ +import GenericStatsColumn from './components/GenericStatsColumn.js'; + +export type Stat = [string, string | number]; + +export default function GenericStats(stats: Stat[], columnCount = 2) { + const itemsPerRow = Math.ceil(stats.length / columnCount); + const columns: Stat[][] = []; + + stats.forEach((stat, index) => { + const column = Math.floor(index / itemsPerRow); + if (!columns[column]) columns[column] = []; + columns[column].push(stat); + }); + + return ` + + +
+ ${columns.map((column) => GenericStatsColumn(column)).join('\n')} +
+ `; +} diff --git a/src/components/Screenshot.ts b/src/components/Screenshot.ts new file mode 100644 index 000000000..3d5d40615 --- /dev/null +++ b/src/components/Screenshot.ts @@ -0,0 +1,26 @@ +import Children from './Children.js'; + +export default function Screenshot(...children: string[]) { + return ` + + +
+ ${Children(children)} +
+ `; +} diff --git a/src/components/TitleBar.ts b/src/components/TitleBar.ts new file mode 100644 index 000000000..b99509609 --- /dev/null +++ b/src/components/TitleBar.ts @@ -0,0 +1,69 @@ +export default function TitleBar( + name: string, + image: string, + nameDiscriminator = '', + description = '', +) { + return ` + + +
+
+ + +
+ + ${name} + ${nameDiscriminator} + + + ${description} +
+
+ + +
+ `; +} diff --git a/src/components/Wrapper.ts b/src/components/Wrapper.ts new file mode 100644 index 000000000..a6fbc4b3c --- /dev/null +++ b/src/components/Wrapper.ts @@ -0,0 +1,21 @@ +import Children from './Children.js'; + +export default function Wrapper(...children: string[]) { + return ` + + +
+ ${Children(children)} +
+ `; +} diff --git a/src/index.ts b/src/index.ts index e12a5cb8d..615c1559d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +import { chromium } from '@playwright/test'; import { Client, GatewayIntentBits } from 'discord.js'; import { args } from './core/process/args.js'; import { registerErrorHandlers } from './events/error.js'; @@ -5,16 +6,12 @@ import guildMemberAdd from './events/guildMemberAdd.js'; import interactionCreate from './events/interactionCreate.js'; import ready from './events/ready.js'; -// sudo apt-get install libatk-bridge2.0-0 -// node node_modules/puppeteer/install.js +registerErrorHandlers(); export const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers], }); -// export const browser = await launch({ headless: 'new' }); -// export const page = await browser.newPage(); - -registerErrorHandlers(); +export const browser = await chromium.launch(); client .on('ready', ready) diff --git a/yarn.lock b/yarn.lock index a3081a5a3..bac853f0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -156,6 +156,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== +"@playwright/test@^1.34.2": + version "1.34.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.34.2.tgz#938ccfb8c162e8ef83dbef3d88ce6851acd469e6" + integrity sha512-v/LBnwzD0225q8xEv3t9DmNPX61yvNnEbiA8PoNk1fbkxApJFCWYLPpQbdVWzHaARdZD9g1PYBoOvnffortfKw== + dependencies: + "@types/node" "*" + playwright-core "1.34.2" + optionalDependencies: + fsevents "2.3.2" + "@sapphire/async-queue@^1.5.0": version "1.5.0" resolved "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz" @@ -184,6 +194,11 @@ resolved "https://registry.yarnpkg.com/@total-typescript/ts-reset/-/ts-reset-0.4.2.tgz#c564c173ba09973968e1046c93965b7a257878a4" integrity sha512-vqd7ZUDSrXFVT1n8b2kc3LnklncDQFPvR58yUS1kEP23/nHPAO9l1lMjUfnPrXYYk4Hj54rrLKMW5ipwk7k09A== +"@types/escape-html@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.2.tgz#072b7b13784fb3cee9c2450c22f36405983f5e3c" + integrity sha512-gaBLT8pdcexFztLSPRtriHeXY/Kn4907uOCZ4Q3lncFBkheAWOuNt53ypsF8szgxbEJ513UeBzcf4utN0EzEwA== + "@types/lodash.startcase@^4.4.7": version "4.4.7" resolved "https://registry.yarnpkg.com/@types/lodash.startcase/-/lodash.startcase-4.4.7.tgz#69857f17be0aa5ce74da4fa386e32e22c012258a" @@ -299,6 +314,11 @@ esbuild@^0.17.5: "@esbuild/win32-ia32" "0.17.19" "@esbuild/win32-x64" "0.17.19" +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" @@ -328,7 +348,7 @@ formdata-polyfill@^4.0.10: dependencies: fetch-blob "^3.1.2" -fsevents@~2.3.2: +fsevents@2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -431,6 +451,11 @@ pkg-types@^1.0.3: mlly "^1.2.0" pathe "^1.1.0" +playwright-core@1.34.2: + version "1.34.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.34.2.tgz#4989ba082f623f21679724033629b2333b626569" + integrity sha512-MrkgGWLANc5qthXduvIY1a/tuafROyTORVd86fwKwgoYrmnBooN/GgeZSBm7ljTLV2FCWNSXV3se7qeScKn83g== + postcss@^8.4.23: version "8.4.23" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab"