Compare commits

...
53 Commits
Author SHA1 Message Date
TrèsAbhi 90e012319a Merge pull request #30 from tresabhi/1.12.7 2023-09-08 21:35:46 -05:00
TresAbhi 18de9cbaab feat: upgraded dependencies 2023-09-09 02:35:14 +00:00
TresAbhi 42a76cbd38 feat: changelogs 2023-09-09 02:19:20 +00:00
TresAbhi eb95634801 fix: escaped markdown for clan event title 2023-09-09 02:11:32 +00:00
TresAbhi de8bfff47f feat: clearer statement about timezones 2023-09-09 02:10:54 +00:00
TresAbhi 07c18911df feat: clearer statement about public data 2023-09-09 02:06:09 +00:00
TresAbhi cc769cc330 fix: react is now defined 2023-09-09 02:04:36 +00:00
TrèsAbhi 9794f58936 Update cd-host.yml 2023-09-08 11:50:31 -05:00
TrèsAbhi a44fcff3b1 Update cd-host.yml 2023-09-08 11:48:41 -05:00
TrèsAbhi 7e4a3e88b9 Update cd-host.yml 2023-09-08 11:47:39 -05:00
TrèsAbhi 8fd45e2205 Merge pull request #29 from tresabhi/1.12.5 2023-09-08 11:45:15 -05:00
TresAbhi 7d235057e8 feat: changelogs 2023-09-08 16:41:06 +00:00
TresAbhi 63a8a67771 fix: better approximation message and players with 0 wins are omitted 2023-09-08 16:38:08 +00:00
TresAbhi bac39ca176 fix: only use players who have played within the last day 2023-09-08 16:30:44 +00:00
TrèsAbhi 626b1e8601 Merge branch '1.13.0' into main 2023-09-07 15:23:10 -05:00
TresAbhi 04086f31b9 fix: spelling 2023-09-07 03:25:07 +00:00
TresAbhi 7037aa9c3b feat: 1.12.5 2023-09-07 03:24:07 +00:00
TresAbhi f5bab8a698 faet: clan-event 2023-09-07 01:24:12 +00:00
TresAbhi ee44b1b1d8 feat: next js 2023-09-07 01:13:03 +00:00
TresAbhi d0f91df95b feat: split bot and server builds 2023-09-04 16:49:38 +00:00
TrèsAbhi dc1c49da6a Merge pull request #27 from tresabhi/main 2023-09-03 13:24:34 -05:00
TrèsAbhi 331040bbdf Merge pull request #26 from tresabhi/1.12.3 2023-09-03 13:21:00 -05:00
TresAbhi 70e1d4b30d feat: changelogs 2023-09-03 18:20:19 +00:00
TresAbhi 03dbd797fb feat: no ongoing season proof 2023-09-03 18:15:10 +00:00
TresAbhi eb4cb1fbfe feat: removed inDevelopment 2023-08-24 00:08:08 +00:00
TrèsAbhi 28172f7f8f Merge pull request #25 from tresabhi/1.12.2 2023-08-23 09:52:48 -05:00
TresAbhi cb5347cbb8 feat: changelog 2023-08-23 14:51:51 +00:00
TresAbhi 1ce0781d2d feat: use application directory 2023-08-23 00:34:37 +00:00
TresAbhi 1257aa76d7 feat: code split bot 2023-08-23 00:33:26 +00:00
TresAbhi 6ec3835896 feat: updated dependencies 2023-08-23 00:18:25 +00:00
TresAbhi 506fe7d9bd feat: removed unused dependencies 2023-08-23 00:17:25 +00:00
TresAbhi 14ef9f6295 feat: removed unused files 2023-08-23 00:17:03 +00:00
TresAbhi 0d76e1a29e feat: added knip 2023-08-23 00:16:39 +00:00
TresAbhi 9e42d7e8f5 feat: removed powered by footer 2023-08-23 00:15:45 +00:00
TresAbhi 364f97332b feat: better and unified tier selection 2023-08-23 00:14:41 +00:00
TresAbhi caa7bb8a65 fix: removed random dev console log 2023-08-22 23:49:34 +00:00
TresAbhi 3e956b34c1 fix: clan name and icon now show up with multi-tank 2023-08-22 23:49:00 +00:00
TresAbhi 038374e5aa fix: changed examples to guide in navbar 2023-08-22 23:47:55 +00:00
TresAbhi dafd54f830 feat: github link 2023-08-22 23:47:35 +00:00
TresAbhi cb65e6b0af fix: corrected order of changelog versions 2023-08-22 23:46:59 +00:00
TresAbhi 4ca8590c83 fix: guide link 2023-08-22 23:46:15 +00:00
TresAbhi c5ef02d7e6 feat: updated dependencies 2023-08-22 23:45:47 +00:00
TrèsAbhi 8175aa8715 Merge pull request #24 from tresabhi/1.12.1 2023-08-11 13:03:52 -05:00
TresAbhi b426634659 feat: changelog 2023-08-11 18:03:41 +00:00
TresAbhi e92da1b700 fix: revert username pattern infrence 2023-08-11 18:01:31 +00:00
TresAbhi 7e0a2e88ee fix: tank id/index mismatch 2023-08-11 17:57:44 +00:00
TresAbhi 16afa8c2d4 feat: url change 2023-08-10 19:48:27 +00:00
TresAbhi 907288455a changed to js 2023-08-10 19:28:53 +00:00
TrèsAbhi 64689e4e3b Update package.json 2023-08-10 14:24:47 -05:00
TresAbhi 294377846a empty commit 2023-08-10 19:23:09 +00:00
TresAbhi 70e4cbaba2 fix: argv undefined 2023-08-10 19:14:43 +00:00
TrèsAbhi 9e2f9526fb Update package.dist.json 2023-08-10 13:58:15 -05:00
TrèsAbhi 84bec20fa5 Update cd-host.yml 2023-08-10 13:55:56 -05:00
72 changed files with 2199 additions and 786 deletions

No files matched your search

+4 -4
View File
@@ -22,11 +22,11 @@ jobs:
- name: Download release assets - name: Download release assets
run: | run: |
DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \ DOWNLOAD_URL=$(curl -s https://api.github.com/repos/tresabhi/blitzkrieg/releases/latest \
| grep "browser_download_url.*dist.zip" \ | grep "browser_download_url.*bot.zip" \
| cut -d : -f 2,3 \ | cut -d : -f 2,3 \
| tr -d \") | tr -d \")
curl -L -o dist.zip $DOWNLOAD_URL curl -L -o bot.zip $DOWNLOAD_URL
unzip dist.zip unzip bot.zip
- name: Run bot - name: Run bot
run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot.js run: NODE_ENV=production GH_TOKEN=${{ secrets.GH_TOKEN }} DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} WARGAMING_APPLICATION_ID=${{ secrets.WARGAMING_APPLICATION_ID }} node --enable-source-maps --trace-warnings bot/index.cjs
+3 -1
View File
@@ -9,6 +9,8 @@ dist.*
yarn-error.log yarn-error.log
commands.txt commands.txt
/test.* /test.*
test.*
/docs/.vitepress/dist /docs/.vitepress/dist
/docs/.vitepress/cache /docs/.vitepress/cache
servers.csv servers.csv
.next/
+21 -8
View File
@@ -1,4 +1,7 @@
import { DefaultTheme, defineConfig } from 'vitepress'; import { compareVersions } from 'compare-versions';
import { readdirSync } from 'fs';
import { parse } from 'path';
import { defineConfig } from 'vitepress';
import { getSidebar } from 'vitepress-plugin-auto-sidebar'; import { getSidebar } from 'vitepress-plugin-auto-sidebar';
export default defineConfig({ export default defineConfig({
@@ -9,17 +12,27 @@ export default defineConfig({
themeConfig: { themeConfig: {
nav: [ nav: [
{ text: 'Home', link: '/' }, { text: 'Home', link: '/' },
{ text: 'Examples', link: '/markdown-examples' }, { text: 'Guide', link: '/guide/about' },
], ],
sidebar: getSidebar({ sidebar: [
collapsed: false, ...getSidebar({
contentDirs: ['guide', 'legal', 'changelogs'], collapsed: false,
contentRoot: '/docs/', contentDirs: ['guide', 'legal'],
}) as DefaultTheme.Sidebar, contentRoot: '/docs/',
}),
{
text: 'Changelogs',
items: readdirSync('./docs/changelogs')
.map((file) => parse(file).name)
.sort(compareVersions)
.map((name) => ({ text: name, link: `/changelogs/${name}.html` })),
},
],
socialLinks: [ socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }, { icon: 'github', link: 'https://github.com/tresabhi/blitzkrieg' },
], ],
}, },
}); });
+8
View File
@@ -0,0 +1,8 @@
# Blitzkrieg 1.12.1
A duo of nasty bugs fixed.
## Fixes
- Fixed a tank index and id mismatch
- Reverted username pattern infrence in command username inputs
+29
View File
@@ -0,0 +1,29 @@
# Blitzkrieg 1.12.2
A much needed room service is here!
## Changes
- Tier selection is now easier
- Tier options across all commands have been unified to a single style
- Tiers are now sorted from `10` to `1`
- Tiers now show up in the format `Tier {tier_number} - {tier_roman_numeral}` (example: `Tier 8 - VIII`)
- Removed powered by footer as links are provided regardless
- Changed invite instructions to instead use application directory
## Fixes
- Guide link on the website is now fixed
- Versions now show up in the correct order in the navigation bar
- Added Blitzkrieg's GitHub link instead of the default Vitepress
- Changed examples to guide in the navigation bar
- Clan name and icon now show up with multi-tank filters
- Removed a random `console.log`
## Technical Changes
- Added `knip`
- Removed a lot of unused files and dependencies
- Updated all dependencies to their latest
- Code split `bot.ts`
- `client` and `octokit` now reside separately
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.12.3
3-day downtime fixed!
## Changes
- Split `/ratings` into `/ratings current` and `/ratings archive`
- `current` now properly handles when there is no ongoing ratings season
- `archive` displays older archived seasons
## Fixes
- The bot is now no ratings seasons tolerant
+7
View File
@@ -0,0 +1,7 @@
# Blitzkrieg 1.12.4
Added very badly-coded temporary `/clan-event` command (without linting)
## Changes
- Added `/clan-event` command
+12
View File
@@ -0,0 +1,12 @@
# Blitzkrieg 1.12.5
Some patch up work
## Changes
- `/clan-event` now is more legible and comes with an approximation warning
## Fixes
- `/clan-event` now uses a more optimized way of fetching large data
- This should fix the issue where too many people using the command broke it
+12
View File
@@ -0,0 +1,12 @@
# Blitzkrieg 1.12.6
Stops making the bot lie!
## Changes
- The approximation warning for `/clan-event` has been made more concise
- Players with `0` wins are now omitted
## Fixes
- Players who haven't player within the current day are now omitted
+13
View File
@@ -0,0 +1,13 @@
# Blitzkrieg 1.12.7
A few fixes and clarifications
## Changes
- `/clan-event`
- Made statement about the limited publicly available data
- Added a statement about reset times
## Fixes
- Commands with images now once again work
+1 -8
View File
@@ -1,10 +1,3 @@
# Inviting the Bot # Inviting the Bot
It's quite simple really. Just follow the steps below: Go to [application directory for Blitzkrieg](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go.
1. Join a Discord server that already has Blitzkrieg installed or [the official Discord Server](https://discord.gg/nDt7AjGJQH).
2. Click on the bot's profile.
3. Click the "Add to Server" button under the bot's name and ID.
4. Select what server you want it in and see what permissions you seem fit for the bot.
Voila! You're ready to go.
+1 -1
View File
@@ -15,7 +15,7 @@ hero:
link: guide/install link: guide/install
- theme: alt - theme: alt
text: Guide text: Guide
link: /guide link: /guide/about
features: features:
- title: Clean and Efficient UI - title: Clean and Efficient UI
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+7
View File
@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
/* config options here */
distDir: 'dist/website',
};
export default nextConfig;
+1 -1
View File
@@ -2,6 +2,6 @@
"main": "server.js", "main": "server.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node --enable-source-maps --trace-warnings server.js" "start": "node --enable-source-maps --trace-warnings server.cjs"
} }
} }
+32 -24
View File
@@ -1,55 +1,63 @@
{ {
"name": "blitzkrieg", "name": "blitzkrieg",
"version": "1.12.0", "version": "1.12.7",
"description": "", "description": "🎉 All-in-one Discord bot for everything World of Tanks Blitz",
"main": "src/server.ts", "main": "src/server.ts",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build:bot": "vite-node --script scripts/build --build=bot", "build": "node scripts/build && yarn build:website",
"build:server": "vite-node --script scripts/build --build=server", "build:bot": "node scripts/build --build=bot",
"build:server": "node scripts/build --build=server",
"build:website": "next build",
"build:docs": "vitepress build docs", "build:docs": "vitepress build docs",
"build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard", "build:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard",
"build": "vite-node --script scripts/build --build=all", "dev:bot": "yarn build:bot && yarn start:bot",
"dev:bot": "yarn build:bot --dev && yarn start:bot", "dev:server": "yarn build:server && yarn start:server",
"dev:server": "yarn build:server --dev && yarn start:server",
"dev:docs": "vitepress dev docs", "dev:docs": "vitepress dev docs",
"start:bot": "node --enable-source-maps --trace-warnings dist/bot.cjs", "dev:website": "next dev",
"start:server": "node --enable-source-maps --trace-warnings dist/server.cjs", "start:bot": "node --enable-source-maps --trace-warnings dist/bot/index.cjs",
"start:server": "node --enable-source-maps --trace-warnings dist/server/index.cjs",
"start:website": "next start",
"host:docs": "vitepress preview docs", "host:docs": "vitepress preview docs",
"publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish", "publish:ratingsleaderboard": "vite-node --script scripts/buildRatingsLeaderboard --publish",
"zip": "zip -r dist.zip dist", "zip": "zip -r dist.zip dist",
"lint": "npx tsc", "lint": "npx tsc",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"" "lint:website": "next lint",
"format": "prettier --write \"**/*.@(json|js|jsx|css|md|ts|tsx|html|xml|yml|gltf)\"",
"knip": "knip",
"update": "yarn upgrade --latest"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/rest": "^20.0.1", "@octokit/rest": "^20.0.1",
"@radix-ui/colors": "^0.1.8", "@radix-ui/colors": "^2.1.0",
"@resvg/resvg-js": "^2.4.1", "@resvg/resvg-js": "^2.4.1",
"bepaint": "^0.1.0-beta.3", "bepaint": "^0.1.0-beta.3",
"discord.js": "^14.12.0", "discord.js": "^14.13.0",
"esbuild": "^0.18.10", "esbuild": "^0.19.2",
"escape-html": "^1.0.3",
"express": "^4.18.2", "express": "^4.18.2",
"false": "^0.0.4",
"fuzzysort": "^2.0.4", "fuzzysort": "^2.0.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-escape": "^2.0.0", "markdown-escape": "^2.0.0",
"next": "^13.4.19",
"react": "^18.2.0", "react": "^18.2.0",
"satori": "^0.9.1", "react-dom": "^18.2.0",
"vite-node": "^0.33.0", "satori": "^0.10.4",
"vite-node": "^0.34.4",
"vitepress-plugin-auto-sidebar": "^1.1.0" "vitepress-plugin-auto-sidebar": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@total-typescript/ts-reset": "^0.4.2", "@total-typescript/ts-reset": "^0.5.1",
"@types/escape-html": "^1.0.2", "@types/escape-html": "^1.0.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/lodash": "^4.14.195", "@types/lodash": "^4.14.198",
"@types/markdown-escape": "^1.1.0", "@types/markdown-escape": "^1.1.0",
"@types/node": "^20.2.1", "@types/node": "^20.6.0",
"@types/react": "^18.2.7", "@types/react": "^18.2.21",
"prettier": "^3.0.0", "compare-versions": "^6.1.0",
"typescript": "^5.0.4", "knip": "^2.22.0",
"vitepress": "^1.0.0-beta.3" "prettier": "^3.0.3",
"typescript": "^5.2.2",
"vitepress": "^1.0.0-rc.10"
} }
} }
+33
View File
@@ -0,0 +1,33 @@
import { build } from 'esbuild';
import { argv } from 'process';
const targetFile = argv
.find((arg) => arg.startsWith('--build='))
?.split('=')[1];
const files = targetFile ? [targetFile] : ['bot', 'server'];
files.forEach(async (file) => {
console.log(`Building ${file}...`);
await build({
entryPoints: [`src/${file}.ts`],
outfile: `dist/${file}/index.cjs`,
platform: 'node',
loader: {
'.node': 'copy',
'.ttf': 'file',
},
format: 'cjs',
bundle: true,
sourcemap: true,
minifyIdentifiers: false, // cause goofy errors
minifySyntax: true,
minifyWhitespace: true,
logLevel: 'info',
});
console.log(`Built ${file}`);
});
-63
View File
@@ -1,63 +0,0 @@
import { BuildOptions, build } from 'esbuild';
import { copyFile, mkdirSync, rmSync } from 'fs';
import { argv } from 'process';
const isProd = !argv.includes('--dev');
const buildAll = argv.includes('--build=all');
const buildBot = argv.includes('--build=bot') || buildAll;
const buildServer = argv.includes('--build=server') || buildAll;
if (isProd) {
// only remove in production to ensure no time is wasted in dev
console.log('Removing dist...');
rmSync('dist', { recursive: true, force: true });
console.log('Dist removed');
console.log('Creating dist...');
mkdirSync('dist');
console.log('Dist created');
console.log('Copying package.json...');
copyFile('package.dist.json', 'dist/package.json', () =>
console.log('package.json copied'),
);
}
const commonOptions: BuildOptions = {
platform: 'node',
loader: {
'.node': 'copy',
'.ttf': 'file',
},
format: 'cjs',
bundle: true,
sourcemap: true,
minifyIdentifiers: false, // causes errors
minifySyntax: isProd,
minifyWhitespace: isProd,
logLevel: isProd ? 'info' : 'silent',
};
if (buildBot) {
console.log('Building bot...');
build({
...commonOptions,
entryPoints: ['src/bot.ts'],
outfile: 'dist/bot.cjs',
}).then(() => console.log('Bot built'));
}
if (buildServer) {
console.log('Building server...');
build({
...commonOptions,
entryPoints: ['src/server.ts'],
outfile: 'dist/server.cjs',
}).then(() => {
console.log('Server built');
});
}
+10 -3
View File
@@ -31,7 +31,11 @@ const PLAYERS = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
) )
.then((response) => response.json() as Promise<RatingsInfo>) .then((response) => response.json() as Promise<RatingsInfo>)
.then((data) => data.count); .then((data) => (data.detail === undefined ? data.count : undefined));
if (PLAYERS === undefined) {
throw new Error('No current ratings season running');
}
const players: Record<number, RatingsPlayer> = {}; const players: Record<number, RatingsPlayer> = {};
const leaderboard: BlitzkriegRatingsLeaderboard = []; const leaderboard: BlitzkriegRatingsLeaderboard = [];
@@ -60,7 +64,7 @@ async function branchFromPlayer(id: number) {
console.log( console.log(
`Registered ${registered} / ${PLAYERS} (${( `Registered ${registered} / ${PLAYERS} (${(
(registered / PLAYERS) * (registered / PLAYERS!) *
100 100
).toFixed(2)}%)`, ).toFixed(2)}%)`,
); );
@@ -109,7 +113,10 @@ if (publish) {
const info = await fetch( const info = await fetch(
`https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`, `https://${server}.wotblitz.com/en/api/rating-leaderboards/season/`,
).then((response) => response.json() as Promise<RatingsInfo>); ).then(
(response) =>
response.json() as Promise<RatingsInfo & { detail: undefined }>,
);
const normalizedServer = server === 'na' ? 'com' : server; const normalizedServer = server === 'na' ? 'com' : server;
const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`; const infoPath = `${normalizedServer}/ratings/${info.current_season}/info.json`;
+31
View File
@@ -0,0 +1,31 @@
import { ReactNode } from 'react';
import Navbar from '../components/Navbar';
interface RootLayoutProps {
children: ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html>
<body
style={{
margin: 0,
}}
>
<div
style={{
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column',
}}
>
<Navbar />
{children}
</div>
</body>
</html>
);
}
+3
View File
@@ -0,0 +1,3 @@
export default function Page() {
return <h1>Hello, Next.js!</h1>;
}
+1 -14
View File
@@ -1,19 +1,6 @@
import { Octokit } from '@octokit/rest'; import { client } from './core/discord/client';
import { Client, GatewayIntentBits } from 'discord.js';
import { secrets } from './core/node/secrets'; import { secrets } from './core/node/secrets';
import { registerErrorHandlers } from './events/error'; import { registerErrorHandlers } from './events/error';
import guildMemberAdd from './events/guildMemberAdd';
import interactionCreate from './events/interactionCreate';
import ready from './events/ready';
export const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
.on('ready', ready)
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate);
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
registerErrorHandlers(); registerErrorHandlers();
client.login(secrets.DISCORD_TOKEN); client.login(secrets.DISCORD_TOKEN);
+97
View File
@@ -0,0 +1,97 @@
import { SlashCommandBuilder, escapeMarkdown } from 'discord.js';
import markdownEscape from 'markdown-escape';
import getWargamingResponse from '../core/blitz/getWargamingResponse';
import { PlayerHistoriesRaw } from '../core/blitzstars/getPlayerHistories';
import getTimeDaysAgo from '../core/blitzstars/getTimeDaysAgo';
import addClanChoices from '../core/discord/addClanChoices';
import autocompleteClan from '../core/discord/autocompleteClan';
import embedWarning from '../core/discord/embedWarning';
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate';
import { AccountAchievements } from '../types/accountAchievements';
import { AccountInfo } from '../types/accountInfo';
import { ClanInfo } from '../types/clanInfo';
export const clanEventCommand: CommandRegistry = {
inProduction: true,
inPublic: true,
command: new SlashCommandBuilder()
.setName('clan-event')
.setDescription('Lists all inactive players')
.addStringOption(addClanChoices),
async handler(interaction) {
const { region, id } = await resolveClanFromCommand(interaction);
const clan = (
await getWargamingResponse<ClanInfo>(
`https://api.wotblitz.${region}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${id}`,
)
)[id];
const memberIds = clan.members_ids.join(',');
const [players, achievements, previousJointVictories] = await Promise.all([
getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${memberIds}`,
),
getWargamingResponse<AccountAchievements>(
`https://api.wotblitz.${region}/wotb/account/achievements/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${memberIds}`,
),
Promise.all(
clan.members_ids.map((id) =>
fetch(`https://www.blitzstars.com/api/playerstats/${id}`)
.then((response) => response.json() as Promise<PlayerHistoriesRaw>)
.then((data) => ({
id,
jointVictory: data[0]?.achievements.jointVictoryCount as
| number
| undefined,
})),
),
).then((data) =>
data.reduce<Record<number, number | undefined>>(
(accumulator, value) => ({
...accumulator,
[value.id]: value.jointVictory,
}),
{},
),
),
]);
const jointVictories = clan.members_ids.map((id) => ({
id,
joinVictory:
players[id].last_battle_time > getTimeDaysAgo(region, 1)
? previousJointVictories[id] === undefined
? 0
: achievements[id].max_series.jointVictory -
previousJointVictories[id]!
: 0,
}));
return [
`# ${escapeMarkdown(clan.name)} [${escapeMarkdown(
clan.tag,
)}]'s platoon wins today: ${(
jointVictories.reduce(
(accumulator, { joinVictory }) => accumulator + joinVictory,
0,
) / 2
).toFixed(0)}\n${jointVictories
.filter(({ joinVictory }) => joinVictory)
.sort((a, b) => b.joinVictory - a.joinVictory)
.map(
({ id, joinVictory }) =>
`- ${markdownEscape(players[id].nickname)}: ${joinVictory}`,
)
.join('\n')}`,
embedWarning(
'This is an approximation!',
"Wargaming provides very little information about platoons publicly. Caveats:\n- Players in two different clans platooning artificially inflates the total count\n- Game-mode battles aren't counted since Wargaming doesn't publicly share that data\n- This command is also subject to Blitzkrieg's reset times (use `/help timezones` for more info)",
),
];
},
autocomplete: autocompleteClan,
};
+1 -2
View File
@@ -1,8 +1,8 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import packageJSON from '../../package.json' assert { type: 'json' }; import packageJSON from '../../package.json' assert { type: 'json' };
import { client } from '../bot';
import { tankopedia } from '../core/blitz/tankopedia'; import { tankopedia } from '../core/blitz/tankopedia';
import { tankAverages } from '../core/blitzstars/tankAverages'; import { tankAverages } from '../core/blitzstars/tankAverages';
import { client } from '../core/discord/client';
import embedInfo from '../core/discord/embedInfo'; import embedInfo from '../core/discord/embedInfo';
import markdownTable from '../core/discord/markdownTable'; import markdownTable from '../core/discord/markdownTable';
import getClientId from '../core/node/getClientId'; import getClientId from '../core/node/getClientId';
@@ -12,7 +12,6 @@ const executionStart = new Date().getTime();
export const debugCommand: CommandRegistry = { export const debugCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
-1
View File
@@ -26,7 +26,6 @@ export const SKILLED_CLANS: Record<SkilledClan, string> = {
export const eligibleCommand: CommandRegistry = { export const eligibleCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: false, inPublic: false,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
-1
View File
@@ -29,7 +29,6 @@ export const evolutionCommand = new Promise<CommandRegistryRaw>(
resolve({ resolve({
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command, command,
+3 -2
View File
@@ -29,7 +29,6 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
resolve({ resolve({
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command, command,
@@ -62,6 +61,8 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
) )
)[player.id]; )[player.id];
const tierOption = interaction.options.getString('tier');
return [ return [
await fullStats( await fullStats(
commandGroup, commandGroup,
@@ -74,7 +75,7 @@ export const fullStatsCommand = new Promise<CommandRegistryRaw>(
'tank-type': 'tank-type':
interaction.options.getString('tank-type') ?? undefined, interaction.options.getString('tank-type') ?? undefined,
nation: interaction.options.getString('nation') ?? undefined, nation: interaction.options.getString('nation') ?? undefined,
tier: interaction.options.getInteger('tier') ?? undefined, tier: tierOption ? parseInt(tierOption) : undefined,
'tree-type': (interaction.options.getString('tree-type') ?? 'tree-type': (interaction.options.getString('tree-type') ??
undefined) as TreeTypeString | undefined, undefined) as TreeTypeString | undefined,
} satisfies Partial<MultiTankFilters>) } satisfies Partial<MultiTankFilters>)
-1
View File
@@ -30,7 +30,6 @@ function addDocsSubcommands(option: SlashCommandSubcommandsOnlyBuilder) {
export const helpCommand: CommandRegistry = { export const helpCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: addDocsSubcommands( command: addDocsSubcommands(
+4 -9
View File
@@ -1,13 +1,12 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import GenericStats from '../components/GenericStats'; import GenericStats from '../components/GenericStats';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import getBlitzClan from '../core/blitz/getBlitzClan';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import addClanChoices from '../core/discord/addClanChoices'; import addClanChoices from '../core/discord/addClanChoices';
import autocompleteClan from '../core/discord/autocompleteClan'; import autocompleteClan from '../core/discord/autocompleteClan';
import resolveClanFromCommand from '../core/discord/resolveClanFromCommand';
import { secrets } from '../core/node/secrets'; import { secrets } from '../core/node/secrets';
import { CommandRegistry } from '../events/interactionCreate'; import { CommandRegistry } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
@@ -17,7 +16,6 @@ const DEFAULT_THRESHOLD = 7;
export const inactiveCommand: CommandRegistry = { export const inactiveCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
@@ -34,19 +32,18 @@ export const inactiveCommand: CommandRegistry = {
), ),
async handler(interaction) { async handler(interaction) {
const clanName = interaction.options.getString('clan')!; const { region, id } = await resolveClanFromCommand(interaction);
const { server, id } = await getBlitzClan(clanName);
const threshold = const threshold =
interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD; interaction.options.getNumber('threshold')! ?? DEFAULT_THRESHOLD;
const time = new Date().getTime() / 1000; const time = new Date().getTime() / 1000;
const clanData = ( const clanData = (
await getWargamingResponse<ClanInfo>( await getWargamingResponse<ClanInfo>(
`https://api.wotblitz.${server}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${id}`, `https://api.wotblitz.${region}/wotb/clans/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&clan_id=${id}`,
) )
)[id]; )[id];
const memberIds = clanData.members_ids; const memberIds = clanData.members_ids;
const accounts = await getWargamingResponse<AccountInfo>( const accounts = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${server}/wotb/account/info/?application_id=${ `https://api.wotblitz.${region}/wotb/account/info/?application_id=${
secrets.WARGAMING_APPLICATION_ID secrets.WARGAMING_APPLICATION_ID
}&account_id=${memberIds.join(',')}`, }&account_id=${memberIds.join(',')}`,
); );
@@ -74,8 +71,6 @@ export const inactiveCommand: CommandRegistry = {
{!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />} {!hasInactiveMembers && <NoData type={NoDataType.PlayersInPeriod} />}
{hasInactiveMembers && <GenericStats stats={inactive} />} {hasInactiveMembers && <GenericStats stats={inactive} />}
<PoweredBy type={PoweredByType.Wargaming} />
</Wrapper> </Wrapper>
); );
}, },
+2 -22
View File
@@ -1,6 +1,5 @@
import { SlashCommandBuilder } from 'discord.js'; import { SlashCommandBuilder } from 'discord.js';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import * as Tanks from '../components/Tanks'; import * as Tanks from '../components/Tanks';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
@@ -15,6 +14,7 @@ import {
tankopedia, tankopedia,
tankopediaInfo, tankopediaInfo,
} from '../core/blitz/tankopedia'; } from '../core/blitz/tankopedia';
import addTierChoices from '../core/discord/addTierChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
@@ -46,30 +46,12 @@ const COMP_TANKS = [
export const ownedTanksCommand: CommandRegistry = { export const ownedTanksCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
.setName('owned-tanks') .setName('owned-tanks')
.setDescription("Shows a player's owned tanks") .setDescription("Shows a player's owned tanks")
.addStringOption((option) => .addStringOption(addTierChoices)
option
.setName('tier')
.setDescription('The tier you want to see')
.setChoices(
{ name: 'Tier I (1)', value: '1' },
{ name: 'Tier II (2)', value: '2' },
{ name: 'Tier III (3)', value: '3' },
{ name: 'Tier IV (4)', value: '4' },
{ name: 'Tier V (5)', value: '5' },
{ name: 'Tier VI (6)', value: '6' },
{ name: 'Tier VII (7)', value: '7' },
{ name: 'Tier VIII (8)', value: '8' },
{ name: 'Tier IX (9)', value: '9' },
{ name: 'Tier X (10)', value: '10' },
)
.setRequired(true),
)
.addStringOption(addUsernameChoices), .addStringOption(addUsernameChoices),
async handler(interaction) { async handler(interaction) {
@@ -170,8 +152,6 @@ export const ownedTanksCommand: CommandRegistry = {
); );
}), }),
))} ))}
<PoweredBy type={PoweredByType.Wargaming} />
</Wrapper> </Wrapper>
); );
}, },
-1
View File
@@ -3,7 +3,6 @@ import { CommandRegistry } from '../events/interactionCreate';
export const pingCommand: CommandRegistry = { export const pingCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
handlesInteraction: true, handlesInteraction: true,
-1
View File
@@ -16,7 +16,6 @@ type SortBy = 'name' | 'count';
export const playerAchievementsCommand: CommandRegistry = { export const playerAchievementsCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
-1
View File
@@ -12,7 +12,6 @@ import { AccountInfo } from '../types/accountInfo';
export const playerInfoCommand: CommandRegistry = { export const playerInfoCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
+358 -113
View File
@@ -1,21 +1,28 @@
import { import {
APIApplicationCommandOptionChoice, APIApplicationCommandOptionChoice,
SlashCommandBuilder, SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js'; } from 'discord.js';
import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard'; import { BlitzkriegRatingsLeaderboard } from '../../scripts/buildRatingsLeaderboard';
import * as Leaderboard from '../components/Leaderboard'; import * as Leaderboard from '../components/Leaderboard';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import { REGION_NAMES_SHORT, Region } from '../constants/regions'; import { REGION_NAMES_SHORT, Region } from '../constants/regions';
import getArchivedRatingsInfo from '../core/blitz/getArchivedRatingsInfo';
import getRatingsInfo from '../core/blitz/getRatingsInfo'; import getRatingsInfo from '../core/blitz/getRatingsInfo';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain'; import regionToRegionSubdomain from '../core/blitz/regionToRegionSubdomain';
import getMidnightLeaderboard from '../core/database/getMidnightLeaderboard'; import getMidnightLeaderboard, {
DATABASE_REPO,
} from '../core/database/getMidnightLeaderboard';
import addRegionChoices from '../core/discord/addRegionChoices'; import addRegionChoices from '../core/discord/addRegionChoices';
import addUsernameChoices from '../core/discord/addUsernameChoices'; import addUsernameChoices from '../core/discord/addUsernameChoices';
import autocompleteUsername from '../core/discord/autocompleteUsername'; import autocompleteUsername from '../core/discord/autocompleteUsername';
import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand'; import resolvePlayerFromCommand from '../core/discord/resolvePlayerFromCommand';
import { octokit } from '../core/express/octokit';
import { secrets } from '../core/node/secrets'; import { secrets } from '../core/node/secrets';
import throwError from '../core/node/throwError';
import { CommandRegistryRaw } from '../events/interactionCreate'; import { CommandRegistryRaw } from '../events/interactionCreate';
import { AccountInfo } from '../types/accountInfo'; import { AccountInfo } from '../types/accountInfo';
import { PlayerClanData } from '../types/playerClanData'; import { PlayerClanData } from '../types/playerClanData';
@@ -86,97 +93,160 @@ export interface RatingsLeague {
percentile: number; percentile: number;
} }
export interface RatingsInfo { export type RatingsInfo =
title: string; | {
icon: string | null; detail: undefined;
start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingsReward[]; title: string;
leagues: RatingsLeague[]; icon: string | null;
} start_at: string;
finish_at: string;
current_season: number;
updated_at: string;
count: number;
rewards: RatingsReward[];
leagues: RatingsLeague[];
}
| { detail: { error: string } };
export interface RatingsNeighbors { export interface RatingsNeighbors {
neighbors: RatingsPlayer[]; neighbors: RatingsPlayer[];
} }
console.log('Caching ratings info...'); const LEAGUES = [
const ratingsInfo = getRatingsInfo('com').then((info) => { { name: 'Diamond', minScore: 5000 },
console.log('Cached ratings info'); { name: 'Platinum', minScore: 4000 },
return info; { name: 'Gold', minScore: 3000 },
}); { name: 'Silver', minScore: 2000 },
{ name: 'Bronze', minScore: 0 },
];
function addSubcommands(
option: SlashCommandSubcommandGroupBuilder,
addOptions = (option: SlashCommandSubcommandBuilder) => option,
) {
return option
.addSubcommand((option) =>
addOptions(
option
.setName('neighbours')
.setDescription('Your position and neighbours'),
)
.addStringOption(addUsernameChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many neighbours to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
)
.addSubcommand((option) =>
addOptions(option.setName('league').setDescription('Top league players'))
.addStringOption((option) =>
option
.setName('league')
.setDescription('The league to display')
.addChoices(
...LEAGUES.map(
({ name }, value) =>
({
name,
value: `${value}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
)
.addStringOption(addRegionChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many rows to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
);
}
export const ratingsCommand = new Promise<CommandRegistryRaw>( export const ratingsCommand = new Promise<CommandRegistryRaw>(
async (resolve) => { async (resolve) => {
const awaitedRatingsInfo = await ratingsInfo; const seasonChoices = await octokit.repos
.getContent({
...DATABASE_REPO,
path: 'com/ratings',
})
.then(({ data }) => {
if (!Array.isArray(data)) {
throw throwError('Archived ratings data is malformed');
}
return data.map(
(folder) =>
({
name: `Season ${folder.name}`,
value: folder.name,
}) satisfies APIApplicationCommandOptionChoice<string>,
);
});
const command = new SlashCommandBuilder() const command = new SlashCommandBuilder()
.setName('ratings') .setName('ratings')
.setDescription('Ratings battles statistics') .setDescription('Ratings battles statistics')
.addSubcommand((option) => .addSubcommandGroup((option) =>
option addSubcommands(
.setName('league') option.setName('archive').setDescription('Any previous season'),
.setDescription('Top league players') (option) =>
.addStringOption((option) => option.addStringOption((option) =>
option option
.setName('league') .setName('season')
.setDescription('The league to display') .setDescription('The season number')
.addChoices( .setChoices(...seasonChoices)
...awaitedRatingsInfo.leagues.map( .setRequired(true),
(league) => ),
({ ),
name: league.title.split(' ')[0].toLowerCase(),
value: `${league.index}`,
}) satisfies APIApplicationCommandOptionChoice<string>,
),
)
.setRequired(true),
)
.addStringOption(addRegionChoices)
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('How many rows to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
) )
.addSubcommand((option) => .addSubcommandGroup((option) =>
option addSubcommands(
.setName('neighbours') option
.setDescription('Your position and neighbours') .setName('current')
.addStringOption(addUsernameChoices) .setDescription('The current running Ratings season'),
.addIntegerOption((option) => ),
option
.setName('limit')
.setDescription('How many neighbours to display (default: 10)')
.setRequired(false)
.setMinValue(5)
.setMaxValue(30),
),
); );
resolve({ resolve({
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command, command,
async handler(interaction) { async handler(interaction) {
const subcommand = interaction.options.getSubcommand(true); interface SimplifiedPlayer {
id: number;
score: number;
position: number;
clan: string | undefined;
nickname: string;
}
const subcommandGroup = interaction.options.getSubcommandGroup(true) as
| 'archive'
| 'current';
const subcommand = interaction.options.getSubcommand(true) as
| 'league'
| 'neighbours';
const limit = interaction.options.getInteger('limit') ?? 10; const limit = interaction.options.getInteger('limit') ?? 10;
const seasonOption = interaction.options.getString('season');
const season = seasonOption ? parseInt(seasonOption) : undefined;
let titleName: string; let titleName: string;
let titleImage: string | undefined; let titleImage: string | undefined;
let titleNameDiscriminator: string | undefined; let titleNameDiscriminator: string | undefined;
let titleDescription: string; let titleDescription: string;
let playersBefore: number; let playersBefore: number;
let playersAfter: number; let playersAfter: number;
let players: RatingsPlayer[]; let players: SimplifiedPlayer[] | undefined;
let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined; let midnightLeaderboard: BlitzkriegRatingsLeaderboard | undefined;
let playerId: number | undefined; let playerId: number | undefined;
let regionRatingsInfo: RatingsInfo; let regionRatingsInfo: RatingsInfo;
@@ -186,31 +256,120 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
interaction.options.getString('league')!, interaction.options.getString('league')!,
); );
const region = interaction.options.getString('region') as Region; const region = interaction.options.getString('region') as Region;
regionRatingsInfo = await getRatingsInfo(region); regionRatingsInfo = await (subcommandGroup === 'current'
? getRatingsInfo(region)
: getArchivedRatingsInfo(season!, region));
const regionSubdomain = regionToRegionSubdomain(region); const regionSubdomain = regionToRegionSubdomain(region);
const result = await fetch( const result =
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`, subcommandGroup === 'current'
) ? regionRatingsInfo.detail === undefined
.then((response) => response.json() as Promise<LeagueTop>) ? await fetch(
.then(({ result }) => result.slice(0, limit)); `https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/league/${leagueIndex}/top/`,
const leagueInfo = regionRatingsInfo.leagues[leagueIndex]; )
midnightLeaderboard = await getMidnightLeaderboard( .then((response) => response.json() as Promise<LeagueTop>)
region, .then(async ({ result }) =>
regionRatingsInfo.current_season, result.slice(0, limit).map(
); (player) =>
({
id: player.spa_id,
score: player.score,
position: player.number,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: undefined
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/latest.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings data is malformed');
}
const players = (await fetch(data.download_url!).then(
(response) => response.json(),
)) as BlitzkriegRatingsLeaderboard;
const firstPlayerIndex =
leagueIndex === 0
? 0
: players.findIndex(
(player) =>
player.score < LEAGUES[leagueIndex - 1].minScore,
);
const lastPlayerIndex = firstPlayerIndex + limit - 1;
const trimmed = players.slice(
firstPlayerIndex,
lastPlayerIndex + 1,
);
const ids = trimmed.map((player) => player.id).join(',');
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
);
return trimmed.map(
(player, index) =>
({
id: player.id,
score: player.score,
position: firstPlayerIndex + index + 1,
clan: clanData[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name,
}) satisfies SimplifiedPlayer,
);
});
const leagueInfo =
regionRatingsInfo.detail === undefined
? regionRatingsInfo.leagues[leagueIndex]
: undefined;
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
players = result; players = result;
playersBefore = result[0].number - 1; playersBefore = result ? result[0].position - 1 : 0;
playersAfter = playersAfter =
regionRatingsInfo.count - result[result.length - 1].number; regionRatingsInfo.detail === undefined && result !== undefined
titleName = `${leagueInfo.title} - ${REGION_NAMES_SHORT[region]}`; ? regionRatingsInfo.count - result[result.length - 1].position
titleImage = leagueInfo.big_icon.startsWith('http') : 0;
? leagueInfo.big_icon titleName = `${
: `https:${leagueInfo.big_icon}`; leagueInfo ? leagueInfo.title : 'No Ongoing Season'
} - ${REGION_NAMES_SHORT[region]}`;
titleImage = leagueInfo
? leagueInfo.big_icon.startsWith('http')
? leagueInfo.big_icon
: `https:${leagueInfo.big_icon}`
: undefined;
titleDescription = `Top ${limit} players`; titleDescription = `Top ${limit} players`;
} else { } else {
const { region, id } = await resolvePlayerFromCommand(interaction); const { region, id } = await resolvePlayerFromCommand(interaction);
regionRatingsInfo = await getRatingsInfo(region); regionRatingsInfo =
subcommandGroup === 'current'
? await getRatingsInfo(region)
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/info.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError('Archived ratings info is malformed');
}
const response = fetch(data.download_url!);
return (await response.then((response) =>
response.json(),
)) as RatingsInfo;
});
const accountInfo = await getWargamingResponse<AccountInfo>( const accountInfo = await getWargamingResponse<AccountInfo>(
`https://api.wotblitz.${region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`, `https://api.wotblitz.${region}/wotb/account/info/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}`,
); );
@@ -220,57 +379,139 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
) )
)[id]?.clan; )[id]?.clan;
const regionSubdomain = regionToRegionSubdomain(region); const regionSubdomain = regionToRegionSubdomain(region);
const { neighbors } = await fetch( const neighbors =
`https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round( subcommandGroup === 'current'
limit / 2, ? regionRatingsInfo.detail === undefined
)}`, ? await fetch(
).then((response) => response.json() as Promise<RatingsNeighbors>); `https://${regionSubdomain}.wotblitz.com/en/api/rating-leaderboards/user/${id}/?neighbors=${Math.round(
midnightLeaderboard = await getMidnightLeaderboard( limit / 2,
region, )}`,
regionRatingsInfo.current_season, )
); .then(
(response) =>
response.json() as Promise<RatingsNeighbors>,
)
.then((players) =>
players.neighbors.map(
(player) =>
({
id: player.spa_id,
position: player.number,
score: player.score,
clan: player.clan_tag,
nickname: player.nickname,
}) satisfies SimplifiedPlayer,
),
)
: undefined
: await octokit.repos
.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/latest.json`,
})
.then(async ({ data }) => {
if (Array.isArray(data) || data.type !== 'file') {
throw throwError(
'Archived ratings latest data is malformed',
);
}
if (clan) titleNameDiscriminator = `[${clan.tag}]`; const response = fetch(data.download_url!);
const players = (await response.then((response) =>
response.json(),
)) as BlitzkriegRatingsLeaderboard;
const playerIndex = players.findIndex(
(player) => player.id === id,
);
if (playerIndex === -1) {
throw throwError(
`${accountInfo[id].nickname} didn't player ratings in season ${season}`,
'This player did not participate in this season or did not get past calibration.',
);
}
const halfRange = Math.round(limit / 2);
const trimmed = players.slice(
playerIndex - halfRange,
playerIndex + halfRange + 1,
);
const ids = trimmed.map((player) => player.id).join(',');
const clanData = await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${region}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${ids}&extra=clan`,
);
return trimmed.map((player, index) => ({
id: player.id,
score: player.score,
position: playerIndex + index + 1,
clan: clanData[player.id]!.clan?.tag,
nickname: clanData[player.id]!.account_name,
}));
});
midnightLeaderboard =
subcommandGroup === 'current'
? regionRatingsInfo.detail === undefined
? await getMidnightLeaderboard(
region,
regionRatingsInfo.current_season,
)
: undefined
: await getMidnightLeaderboard(region, season!);
if (clan && neighbors) titleNameDiscriminator = `[${clan.tag}]`;
players = neighbors; players = neighbors;
titleImage = clan titleImage = neighbors
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` ? clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png`
: undefined
: undefined; : undefined;
titleName = accountInfo[id].nickname; titleName = neighbors
? accountInfo[id].nickname
: `No Ongoing Season - ${REGION_NAMES_SHORT[region]}`;
titleDescription = 'Ratings neighbours'; titleDescription = 'Ratings neighbours';
playersBefore = neighbors[0].number - 1; playersBefore = neighbors ? neighbors[0].position - 1 : 0;
playersAfter = playersAfter =
regionRatingsInfo.count - neighbors[neighbors.length - 1].number; regionRatingsInfo.detail === undefined
? neighbors
? regionRatingsInfo.count -
neighbors[neighbors.length - 1].position
: 0
: 0;
playerId = id; playerId = id;
} }
const items = players.map((player) => { const items = players?.map((player) => {
const reward = regionRatingsInfo.rewards.find( const reward =
(reward) => regionRatingsInfo.detail === undefined
player.number >= reward.from_position && ? regionRatingsInfo.rewards.find(
player.number <= reward.to_position, (reward) =>
); player.position >= reward.from_position &&
player.position <= reward.to_position,
)
: undefined;
const midnightIndex = midnightLeaderboard?.findIndex( const midnightIndex = midnightLeaderboard?.findIndex(
(item) => item.id === player.spa_id, (item) => item.id === player.id,
); );
const midnightScore = const midnightScore =
midnightIndex === undefined midnightIndex === undefined
? player.score ? player.score
: midnightLeaderboard![midnightIndex].score; : midnightLeaderboard![midnightIndex].score;
const midnightPosition = const midnightPosition =
midnightIndex === undefined ? player.number : midnightIndex + 1; midnightIndex === undefined ? player.position : midnightIndex + 1;
return ( return (
<Leaderboard.Item <Leaderboard.Item
nickname={player.nickname} nickname={player.nickname}
score={player.score} score={player.score}
position={player.number} position={player.position}
clan={player.clan_tag} clan={player.clan}
reward={reward} reward={reward}
deltaScore={player.score - midnightScore} deltaScore={player.score - midnightScore}
deltaPosition={midnightPosition - player.number} deltaPosition={midnightPosition - player.position}
key={player.spa_id} key={player.id}
highlight={player.spa_id === playerId} highlight={player.id === playerId}
/> />
); );
}); });
@@ -284,11 +525,15 @@ export const ratingsCommand = new Promise<CommandRegistryRaw>(
description={`${titleDescription} • ${new Date().toDateString()}`} description={`${titleDescription} • ${new Date().toDateString()}`}
/> />
<Leaderboard.Root> {items && (
{playersBefore > 0 && <Leaderboard.Gap number={playersBefore} />} <Leaderboard.Root>
{items} {playersBefore > 0 && (
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />} <Leaderboard.Gap number={playersBefore} />
</Leaderboard.Root> )}
{items}
{playersAfter > 0 && <Leaderboard.Gap number={playersAfter} />}
</Leaderboard.Root>
)}
</Wrapper> </Wrapper>
); );
}, },
-1
View File
@@ -11,7 +11,6 @@ import { ClanList } from '../types/clanList';
export const searchClansCommand: CommandRegistry = { export const searchClansCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
-1
View File
@@ -12,7 +12,6 @@ import { AccountList } from '../types/accountList';
export const searchPlayersCommand: CommandRegistry = { export const searchPlayersCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
-1
View File
@@ -8,7 +8,6 @@ import { CommandRegistry } from '../events/interactionCreate';
export const searchTanksCommand: CommandRegistry = { export const searchTanksCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
+3 -2
View File
@@ -27,7 +27,6 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
resolve({ resolve({
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command, command,
@@ -60,6 +59,8 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
) )
)[player.id]; )[player.id];
const tierOption = interaction.options.getString('tier');
return [ return [
await stats( await stats(
commandGroup, commandGroup,
@@ -72,7 +73,7 @@ export const statsCommand = new Promise<CommandRegistryRaw>(async (resolve) => {
'tank-type': 'tank-type':
interaction.options.getString('tank-type') ?? undefined, interaction.options.getString('tank-type') ?? undefined,
nation: interaction.options.getString('nation') ?? undefined, nation: interaction.options.getString('nation') ?? undefined,
tier: interaction.options.getInteger('tier') ?? undefined, tier: tierOption ? parseInt(tierOption) : undefined,
'tree-type': (interaction.options.getString('tree-type') ?? 'tree-type': (interaction.options.getString('tree-type') ??
undefined) as TreeTypeString | undefined, undefined) as TreeTypeString | undefined,
} satisfies Partial<MultiTankFilters>) } satisfies Partial<MultiTankFilters>)
-1
View File
@@ -15,7 +15,6 @@ import { AccountInfo } from '../types/accountInfo';
export const todayCommand: CommandRegistry = { export const todayCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
-1
View File
@@ -14,7 +14,6 @@ import { PlayerClanData } from '../types/playerClanData';
export const verifyCommand: CommandRegistry = { export const verifyCommand: CommandRegistry = {
inProduction: true, inProduction: true,
inDevelopment: true,
inPublic: true, inPublic: true,
command: new SlashCommandBuilder() command: new SlashCommandBuilder()
+5
View File
@@ -0,0 +1,5 @@
export default function Navbar() {
return (
<div style={{ display: 'flex', height: 100, backgroundColor: 'red' }}></div>
);
}
-43
View File
@@ -1,43 +0,0 @@
import { ReactNode } from 'react';
import { theme } from '../stitches.config';
export enum PoweredByType {
BlitzStars,
Wargaming,
}
export interface PoweredByProps {
type: PoweredByType;
footer?: ReactNode;
}
const POWERED_BY_IMAGES: Record<PoweredByType, string> = {
[PoweredByType.BlitzStars]:
'https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png',
[PoweredByType.Wargaming]: 'https://i.imgur.com/jADGWm0.png',
};
const POWERED_BY_NAMES: Record<PoweredByType, string> = {
[PoweredByType.BlitzStars]: 'BlitzStars',
[PoweredByType.Wargaming]: 'Wargaming',
};
export default function PoweredBy({ type, footer }: PoweredByProps) {
return (
<div
style={{
display: 'flex',
justifyContent: footer === undefined ? 'space-around' : 'space-between',
}}
>
{footer !== undefined && <div style={{ display: 'flex' }}>{footer}</div>}
<div style={{ display: 'flex', gap: 4 }}>
<img src={POWERED_BY_IMAGES[type]} style={{ height: 16, width: 16 }} />
<span style={{ color: theme.colors.textLowContrast, fontSize: 16 }}>
Powered by {POWERED_BY_NAMES[type]}
</span>
</div>
</div>
);
}
+1 -1
View File
@@ -1 +1 @@
export const CYCLIC_API = 'https://blitzkrieg.cyclic.app'; export const CYCLIC_API = 'https://blitzkrieg-embeds.cyclic.app';
-30
View File
@@ -1,30 +0,0 @@
import { AllStats } from '../../types/accountInfo';
export default function diffStats(stats1: AllStats, stats2: AllStats) {
function diff(value: (allStats: AllStats) => number) {
return value(stats2) - value(stats1);
}
function max(value: (allStats: AllStats) => number) {
return Math.max(value(stats1), value(stats2));
}
return {
battles: diff((a) => a.battles),
wins: diff((a) => a.wins),
capture_points: diff((a) => a.capture_points),
damage_dealt: diff((a) => a.damage_dealt),
damage_received: diff((a) => a.damage_received),
dropped_capture_points: diff((a) => a.dropped_capture_points),
frags: diff((a) => a.frags),
frags8p: diff((a) => a.frags8p),
hits: diff((a) => a.hits),
losses: diff((a) => a.losses),
max_frags: max((a) => a.max_frags),
max_xp: max((a) => a.max_xp),
shots: diff((a) => a.shots),
spotted: diff((a) => a.spotted),
survived_battles: diff((a) => a.survived_battles),
win_and_survived: diff((a) => a.win_and_survived),
xp: diff((a) => a.xp),
} satisfies AllStats;
}
+25
View File
@@ -0,0 +1,25 @@
import { RatingsInfo } from '../../commands/ratings';
import { Region } from '../../constants/regions';
import { DATABASE_REPO } from '../database/getMidnightLeaderboard';
import { octokit } from '../express/octokit';
import throwError from '../node/throwError';
export default async function getArchivedRatingsInfo(
season: number,
region: Region,
) {
const { data } = await octokit.repos.getContent({
...DATABASE_REPO,
path: `${region}/ratings/${season}/info.json`,
});
if (Array.isArray(data) || data.type !== 'file')
throw throwError('Archived ratings info is malformed');
const content = Buffer.from(data.content, 'base64').toString();
const jsonContent = JSON.parse(content) as RatingsInfo & {
detail: undefined;
};
return jsonContent;
}
-22
View File
@@ -1,22 +0,0 @@
import { Region } from '../../constants/regions';
import { serverAndIdPattern } from '../discord/resolvePlayerFromCommand';
import throwError from '../node/throwError';
import listClans from './listClans';
export default async function getBlitzClan(clan: string) {
if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split('/');
return { server: server as Region, id: Number(accountId) };
} else {
const accounts = await listClans(clan);
if (accounts[0]) {
return { server: accounts[0].server, id: accounts[0].clan_id };
} else {
throw throwError(
'Could not find clan',
`I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
);
}
}
}
+1
View File
@@ -8,5 +8,6 @@ export default async function getRatingsInfo(region: Region) {
region, region,
)}.wotblitz.com/en/api/rating-leaderboards/season/`, )}.wotblitz.com/en/api/rating-leaderboards/season/`,
); );
return (await response.json()) as Promise<RatingsInfo>; return (await response.json()) as Promise<RatingsInfo>;
} }
+5 -5
View File
@@ -3,8 +3,8 @@ import { Clan, ClanList } from '../../types/clanList';
import { secrets } from '../node/secrets'; import { secrets } from '../node/secrets';
import getWargamingResponse from './getWargamingResponse'; import getWargamingResponse from './getWargamingResponse';
export type ClanListWithServer = (Clan & { export type ClanListWithRegion = (Clan & {
server: 'com' | 'eu' | 'asia'; region: 'com' | 'eu' | 'asia';
})[]; })[];
export default async function listClans(search: string, limit = 9) { export default async function listClans(search: string, limit = 9) {
@@ -21,7 +21,7 @@ export default async function listClans(search: string, limit = 9) {
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'com' as Region, region: 'com' as Region,
})), })),
), ),
getWargamingResponse<ClanList>( getWargamingResponse<ClanList>(
@@ -31,7 +31,7 @@ export default async function listClans(search: string, limit = 9) {
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'eu' as Region, region: 'eu' as Region,
})), })),
), ),
getWargamingResponse<ClanList>( getWargamingResponse<ClanList>(
@@ -41,7 +41,7 @@ export default async function listClans(search: string, limit = 9) {
value && value &&
value.map((account) => ({ value.map((account) => ({
...account, ...account,
server: 'asia' as Region, region: 'asia' as Region,
})), })),
), ),
]) ])
+32
View File
@@ -0,0 +1,32 @@
/**
* Thanks, August!
* https://stackoverflow.com/a/41358305/12294756
*/
const ROMAN = {
M: 1000,
CM: 900,
D: 500,
CD: 400,
C: 100,
XC: 90,
L: 50,
XL: 40,
X: 10,
IX: 9,
V: 5,
IV: 4,
I: 1,
};
const ROMAN_KEYS = Object.keys(ROMAN) as (keyof typeof ROMAN)[];
export default function numberToRomanNumeral(value: number) {
let string = '';
ROMAN_KEYS.forEach((index) => {
const part = Math.floor(value / ROMAN[index]);
value -= part * ROMAN[index];
string += index.repeat(part);
});
return string;
}
+3 -1
View File
@@ -40,13 +40,15 @@ export const TANK_NAMES = new Promise<string[]>(async (resolve) => {
export const TANK_NAMES_DIACRITICS = TANK_NAMES.then((tankNames) => export const TANK_NAMES_DIACRITICS = TANK_NAMES.then((tankNames) =>
Promise.all( Promise.all(
tankNames.map(async (tankName, index) => { tankNames.map(async (tankName, index) => {
const name = tankName ?? `Unknown Tank ${(await TANKS)[index].tank_id}`; const id = (await TANKS)[index].tank_id;
const name = tankName ?? `Unknown Tank ${id}`;
const diacriticless = deburr(name); const diacriticless = deburr(name);
return { return {
original: name, original: name,
diacriticless, diacriticless,
combined: `${name}${diacriticless}`, combined: `${name}${diacriticless}`,
id,
}; };
}), }),
), ),
-12
View File
@@ -1,12 +0,0 @@
import sumStats from '../blitz/sumStats';
import { emptyAllStats } from './getDiffedTankStats';
import { tankAverages } from './tankAverages';
console.log('Calculating player average...');
export const playerAverage = Object.entries(await tankAverages).reduce(
(accumulator, [, tankAverage]) => {
return sumStats([accumulator, tankAverage.all]);
},
emptyAllStats,
);
console.log('Calculated player average');
+6 -5
View File
@@ -1,6 +1,6 @@
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard'; import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
import { octokit } from '../../bot';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import { octokit } from '../express/octokit';
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' }; export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
@@ -15,12 +15,13 @@ export default async function getMidnightLeaderboard(
}); });
if (!Array.isArray(data) && data.type === 'file') { if (!Array.isArray(data) && data.type === 'file') {
const content = Buffer.from(data.content, 'base64').toString(); const response = await fetch(data.download_url!);
const jsonContent = JSON.parse(content) as BlitzkriegRatingsLeaderboard;
return jsonContent; return (await response.json()) as BlitzkriegRatingsLeaderboard;
} }
} catch (error) { } catch (error) {
console.warn('No midnight leaderboard found'); console.warn(
`No midnight leaderboard found for season ${season}, region ${region}`,
);
} }
} }
@@ -4,6 +4,7 @@ import { StatType } from '../../renderers/stats';
import { tankopediaInfo } from '../blitz/tankopedia'; import { tankopediaInfo } from '../blitz/tankopedia';
import addPeriodSubCommands from './addPeriodSubCommands'; import addPeriodSubCommands from './addPeriodSubCommands';
import addTankChoices from './addTankChoices'; import addTankChoices from './addTankChoices';
import addTierChoices from './addTierChoices';
import addUsernameChoices from './addUsernameChoices'; import addUsernameChoices from './addUsernameChoices';
export default async function addStatTypeSubCommandGroups( export default async function addStatTypeSubCommandGroups(
@@ -61,13 +62,8 @@ export default async function addStatTypeSubCommandGroups(
) )
.setRequired(false), .setRequired(false),
) )
.addIntegerOption((option) => .addStringOption((option) =>
option addTierChoices(option).setRequired(false),
.setName('tier')
.setDescription('Tier')
.setMinValue(1)
.setMaxValue(10)
.setRequired(false),
) )
.addStringOption((option) => .addStringOption((option) =>
option option
+16
View File
@@ -0,0 +1,16 @@
import { SlashCommandStringOption } from 'discord.js';
import { range } from 'lodash';
import numberToRomanNumeral from '../blitz/numberToRomanNumeral';
export default function addTierChoices(option: SlashCommandStringOption) {
return option
.setName('tier')
.setDescription('The tier you want to see')
.setChoices(
...range(10, 0).map((tier) => ({
name: `Tier ${tier} - ${numberToRomanNumeral(tier)}`,
value: `${tier}`,
})),
)
.setRequired(true);
}
+2 -2
View File
@@ -19,8 +19,8 @@ export default async function autocompleteClan(
? players.map( ? players.map(
(player) => (player) =>
({ ({
name: `${player.name} (${REGION_NAMES[player.server]})`, name: `${player.name} (${REGION_NAMES[player.region]})`,
value: `${player.server}/${player.clan_id}`, value: `${player.region}/${player.clan_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>, }) satisfies ApplicationCommandOptionChoiceData<string>,
) )
: [], : [],
+3 -3
View File
@@ -4,7 +4,7 @@ import {
CacheType, CacheType,
} from 'discord.js'; } from 'discord.js';
import { go } from 'fuzzysort'; import { go } from 'fuzzysort';
import { TANKS, TANK_NAMES_DIACRITICS } from '../blitz/tankopedia'; import { TANK_NAMES_DIACRITICS } from '../blitz/tankopedia';
export default async function autocompleteTanks( export default async function autocompleteTanks(
interaction: AutocompleteInteraction<CacheType>, interaction: AutocompleteInteraction<CacheType>,
@@ -19,10 +19,10 @@ export default async function autocompleteTanks(
keys: ['combined'], keys: ['combined'],
limit: 10, limit: 10,
}).map( }).map(
async (item, index) => async (item) =>
({ ({
name: item.obj.original, name: item.obj.original,
value: `${(await TANKS)[index].tank_id}`, value: `${item.obj.id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>, }) satisfies ApplicationCommandOptionChoiceData<string>,
), ),
) )
+11
View File
@@ -0,0 +1,11 @@
import { Client, GatewayIntentBits } from 'discord.js';
import guildMemberAdd from '../../events/guildMemberAdd';
import interactionCreate from '../../events/interactionCreate';
import ready from '../../events/ready';
export const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
.on('ready', ready)
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate);
@@ -0,0 +1,27 @@
import { ChatInputCommandInteraction } from 'discord.js';
import { Region } from '../../constants/regions';
import listClans from '../blitz/listClans';
import throwError from '../node/throwError';
import { serverAndIdPattern } from './resolvePlayerFromCommand';
export default async function resolveClanFromCommand(
interaction: ChatInputCommandInteraction,
) {
const clan = interaction.options.getString('clan', true);
if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split('/');
return { region: server as Region, id: Number(accountId) };
} else {
const accounts = await listClans(clan);
if (accounts[0]) {
return { region: accounts[0].region, id: accounts[0].clan_id };
} else {
throw throwError(
'Could not find clan',
`I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
);
}
}
}
+1 -2
View File
@@ -1,7 +1,6 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js'; import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { Region } from '../../constants/regions'; import { Region } from '../../constants/regions';
import listPlayers, { import listPlayers, {
usernamePattern,
usernamePatternWithoutPosition, usernamePatternWithoutPosition,
} from '../blitz/listPlayers'; } from '../blitz/listPlayers';
import throwError from '../node/throwError'; import throwError from '../node/throwError';
@@ -20,7 +19,7 @@ export default async function resolvePlayerFromCommand(
const displayName = interaction.guild?.members.cache const displayName = interaction.guild?.members.cache
.get(interaction.user.id) .get(interaction.user.id)
?.displayName.match(usernamePatternWithoutPosition)?.[0]; ?.displayName.match(usernamePatternWithoutPosition)?.[0];
const username = commandUsername?.match(usernamePattern)?.[0] ?? displayName; const username = commandUsername ?? displayName;
if (username === undefined) { if (username === undefined) {
throw throwError( throw throwError(
-7
View File
@@ -1,7 +0,0 @@
import { ButtonBuilder, ButtonStyle } from 'discord.js';
// TODO: ADD THIS AGAIN IMMEDIATELY
export const supportBlitzStars = new ButtonBuilder()
.setLabel('Support BlitzStars')
.setURL('https://www.blitzstars.com/supporters')
.setStyle(ButtonStyle.Link);
+4
View File
@@ -0,0 +1,4 @@
import { Octokit } from '@octokit/rest';
import { secrets } from '../node/secrets';
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
+1 -1
View File
@@ -1,7 +1,7 @@
import { EmbedBuilder, TextChannel } from 'discord.js'; import { EmbedBuilder, TextChannel } from 'discord.js';
import discord from '../../discord.json' assert { type: 'json' }; import discord from '../../discord.json' assert { type: 'json' };
import { client } from '../bot';
import { NEGATIVE_COLOR } from '../constants/colors'; import { NEGATIVE_COLOR } from '../constants/colors';
import { client } from '../core/discord/client';
import isDev from '../core/node/isDev'; import isDev from '../core/node/isDev';
const PROCESS_ERROR_EVENTS = ['uncaughtException']; const PROCESS_ERROR_EVENTS = ['uncaughtException'];
+7 -8
View File
@@ -13,6 +13,7 @@ import {
SlashCommandSubcommandsOnlyBuilder, SlashCommandSubcommandsOnlyBuilder,
} from 'discord.js'; } from 'discord.js';
import discord from '../../../discord.json' assert { type: 'json' }; import discord from '../../../discord.json' assert { type: 'json' };
import { clanEventCommand } from '../../commands/clanEvent';
import { debugCommand } from '../../commands/debug'; import { debugCommand } from '../../commands/debug';
import { eligibleCommand } from '../../commands/eligible'; import { eligibleCommand } from '../../commands/eligible';
import { evolutionCommand } from '../../commands/evolution'; import { evolutionCommand } from '../../commands/evolution';
@@ -50,7 +51,6 @@ export type InteractionReturnable =
| Promise<InteractionIterableReturnable>; | Promise<InteractionIterableReturnable>;
export interface Registry { export interface Registry {
inDevelopment: boolean;
inProduction: boolean; inProduction: boolean;
} }
@@ -99,6 +99,7 @@ export const COMMANDS_RAW: CommandRegistry[] = [
evolutionCommand, evolutionCommand,
statsCommand, statsCommand,
ratingsCommand, ratingsCommand,
clanEventCommand,
]; ];
export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) => export const commands = Promise.all(COMMANDS_RAW).then((rawCommands) =>
@@ -122,14 +123,12 @@ export const publicCommands: RESTPostAPIChatInputApplicationCommandsJSONBody[] =
commands.then((awaitedCommands) => { commands.then((awaitedCommands) => {
Object.entries(awaitedCommands).forEach(([, registry]) => { Object.entries(awaitedCommands).forEach(([, registry]) => {
if (isDev() ? registry.inDevelopment : registry.inProduction) { const json = registry.command.toJSON();
const json = registry.command.toJSON();
if (registry.inPublic) { if (registry.inPublic) {
publicCommands.push(json); publicCommands.push(json);
} else { } else {
guildCommands.push(json); guildCommands.push(json);
}
} }
}); });
-3
View File
@@ -1,6 +1,5 @@
import * as Graph from '../components/Graph'; import * as Graph from '../components/Graph';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import getWargamingResponse from '../core/blitz/getWargamingResponse'; import getWargamingResponse from '../core/blitz/getWargamingResponse';
@@ -118,8 +117,6 @@ export default async function evolution<Type extends StatType>(
)} )}
{plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />} {plot.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
<PoweredBy type={PoweredByType.BlitzStars} />
</Wrapper> </Wrapper>
); );
} }
+1 -5
View File
@@ -1,6 +1,5 @@
import GenericAllStats from '../components/GenericAllStats'; import GenericAllStats from '../components/GenericAllStats';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TierWeights, { TierWeightsRecord } from '../components/TierWeights'; import TierWeights, { TierWeightsRecord } from '../components/TierWeights';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
@@ -35,7 +34,7 @@ export default async function fullStats<Type extends StatType>(
let nameDiscriminator: string | undefined; let nameDiscriminator: string | undefined;
let image: string | undefined; let image: string | undefined;
if (type === 'player') { if (type === 'player' || type === 'multi-tank') {
const clan = ( const clan = (
await getWargamingResponse<PlayerClanData>( await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`, `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
@@ -49,7 +48,6 @@ export default async function fullStats<Type extends StatType>(
} else if (type === 'tank') { } else if (type === 'tank') {
nameDiscriminator = `(${await resolveTankName(filters as number)})`; nameDiscriminator = `(${await resolveTankName(filters as number)})`;
image = (await tankopedia)[filters as number]?.images.normal; image = (await tankopedia)[filters as number]?.images.normal;
} else {
} }
const { diffed, order } = await getDiffedTankStats(server, id, start, end); const { diffed, order } = await getDiffedTankStats(server, id, start, end);
@@ -165,8 +163,6 @@ export default async function fullStats<Type extends StatType>(
supplementaryStats={supplementaryStats} supplementaryStats={supplementaryStats}
/> />
)} )}
<PoweredBy type={PoweredByType.BlitzStars} />
</Wrapper> </Wrapper>
); );
} }
+1 -4
View File
@@ -1,6 +1,5 @@
import AllStatsOverview from '../components/AllStatsOverview'; import AllStatsOverview from '../components/AllStatsOverview';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import { TreeTypeString } from '../components/Tanks'; import { TreeTypeString } from '../components/Tanks';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
@@ -45,7 +44,7 @@ export default async function stats<Type extends StatType>(
let nameDiscriminator: string | undefined; let nameDiscriminator: string | undefined;
let image: string | undefined; let image: string | undefined;
if (type === 'player') { if (type === 'player' || type === 'multi-tank') {
const clan = ( const clan = (
await getWargamingResponse<PlayerClanData>( await getWargamingResponse<PlayerClanData>(
`https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`, `https://api.wotblitz.${server}/wotb/clans/accountinfo/?application_id=${secrets.WARGAMING_APPLICATION_ID}&account_id=${id}&extra=clan`,
@@ -168,8 +167,6 @@ export default async function stats<Type extends StatType>(
{!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />} {!stats?.battles && <NoData type={NoDataType.BattlesInPeriod} />}
{stats?.battles > 0 && overview} {stats?.battles > 0 && overview}
<PoweredBy type={PoweredByType.BlitzStars} footer={footer} />
</Wrapper> </Wrapper>
); );
} }
-5
View File
@@ -1,6 +1,5 @@
import * as Breakdown from '../components/Breakdown'; import * as Breakdown from '../components/Breakdown';
import NoData, { NoDataType } from '../components/NoData'; import NoData, { NoDataType } from '../components/NoData';
import PoweredBy, { PoweredByType } from '../components/PoweredBy';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import Wrapper from '../components/Wrapper'; import Wrapper from '../components/Wrapper';
import calculateWN8 from '../core/blitz/calculateWN8'; import calculateWN8 from '../core/blitz/calculateWN8';
@@ -27,8 +26,6 @@ export default async function today(
showTotal = true, showTotal = true,
naked?: boolean, naked?: boolean,
) { ) {
console.log(maximized);
const { diffed, order } = await getDiffedTankStats( const { diffed, order } = await getDiffedTankStats(
server, server,
id, id,
@@ -174,8 +171,6 @@ export default async function today(
{rows.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />} {rows.length === 0 && <NoData type={NoDataType.BattlesInPeriod} />}
{rows.length > 0 && <Breakdown.Root>{rows}</Breakdown.Root>} {rows.length > 0 && <Breakdown.Root>{rows}</Breakdown.Root>}
<PoweredBy type={PoweredByType.BlitzStars} />
</Wrapper> </Wrapper>
); );
} }
-1
View File
@@ -6,7 +6,6 @@ import { RouteRegistry } from '../server';
export const statsRoute: RouteRegistry = { export const statsRoute: RouteRegistry = {
route: '/stats/:type/:period', route: '/stats/:type/:period',
inProduction: true, inProduction: true,
inDevelopment: true,
async handler(req) { async handler(req) {
const type = req.params.type as StatType; const type = req.params.type as StatType;
-1
View File
@@ -5,7 +5,6 @@ import { RouteRegistry } from '../server';
export const todayRoute: RouteRegistry = { export const todayRoute: RouteRegistry = {
route: '/today', route: '/today',
inProduction: true, inProduction: true,
inDevelopment: true,
async handler(req) { async handler(req) {
const player = resolvePlayerFromRequest(req); const player = resolvePlayerFromRequest(req);
-3
View File
@@ -1,7 +1,6 @@
import express, { Request, Response } from 'express'; import express, { Request, Response } from 'express';
import normalizeRouteReturnable from './core/express/normalizeRouteReturnable'; import normalizeRouteReturnable from './core/express/normalizeRouteReturnable';
import withAutoRefresh from './core/express/withAutoRefresh'; import withAutoRefresh from './core/express/withAutoRefresh';
import isDev from './core/node/isDev';
import { Registry } from './events/interactionCreate'; import { Registry } from './events/interactionCreate';
import { statsRoute } from './routes/stats'; import { statsRoute } from './routes/stats';
import { todayRoute } from './routes/today'; import { todayRoute } from './routes/today';
@@ -22,8 +21,6 @@ export interface RouteRegistry<HandlesInteraction extends boolean = false>
const app = express(); const app = express();
([statsRoute, todayRoute] as RouteRegistry[]).forEach((registry) => { ([statsRoute, todayRoute] as RouteRegistry[]).forEach((registry) => {
if (!(isDev() ? registry.inDevelopment : registry.inProduction)) return;
app.get(registry.route, async (req, res) => { app.get(registry.route, async (req, res) => {
console.log(req.originalUrl); console.log(req.originalUrl);
+12 -2
View File
@@ -9,7 +9,17 @@
"skipLibCheck": true, "skipLibCheck": true,
"noEmit": true, "noEmit": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"jsx": "react-jsx" "jsx": "react-jsx",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"incremental": true,
"isolatedModules": true,
"plugins": [
{
"name": "next"
}
]
}, },
"include": ["src/**/*.ts", "src/components/Tanks/components/Row.tsx"] "include": ["src/**/*", ".next/types/**/*.ts", "dist/website/types/**/*.ts"],
"exclude": ["node_modules"]
} }
+1317 -325
View File
File diff suppressed because it is too large. Load diff