From 514999e5884a387e13bdc70d5ce9af4e24a55750 Mon Sep 17 00:00:00 2001 From: tresabhi <43380238+tresabhi@users.noreply.github.com> Date: Fri, 31 Jul 2026 20:17:57 -0500 Subject: [PATCH] remove bot --- .gitignore | 1 - README.md | 19 +- docs/guides/introduction.md | 11 - docs/guides/invite.md | 3 - package.json | 3 - packages/bot/package.json | 35 - packages/bot/src/commands/about.ts | 68 -- packages/bot/src/commands/breakdown/index.tsx | 65 -- .../bot/src/commands/breakdown/render.tsx | 295 --------- packages/bot/src/commands/debug.ts | 37 -- packages/bot/src/commands/evolution.tsx | 182 ----- packages/bot/src/commands/fullStats.tsx | 117 ---- packages/bot/src/commands/inactive.tsx | 96 --- packages/bot/src/commands/link.tsx | 193 ------ packages/bot/src/commands/ownedTanks.tsx | 90 --- packages/bot/src/commands/permissions.ts | 46 -- packages/bot/src/commands/ping.ts | 34 - packages/bot/src/commands/playerInfo.ts | 69 -- packages/bot/src/commands/rating.tsx | 535 --------------- .../bot/src/commands/ratingLeaderboard.tsx | 468 ------------- packages/bot/src/commands/replay.tsx | 592 ----------------- packages/bot/src/commands/research.tsx | 626 ------------------ packages/bot/src/commands/searchClans.ts | 74 --- packages/bot/src/commands/searchPlayers.ts | 83 --- packages/bot/src/commands/stats.tsx | 102 --- packages/bot/src/commands/today.ts | 60 -- .../components/HeroStat/components/Glow.tsx | 50 -- .../components/HeroStat/index.tsx | 72 -- .../components/WN8SurroundingStat.tsx | 47 -- .../src/components/AllStatsOverview/index.tsx | 130 ---- .../components/Breakdown/components/Root.tsx | 16 - .../components/Breakdown/components/Row.tsx | 126 ---- .../Breakdown/components/RowStat.tsx | 89 --- .../bot/src/components/Breakdown/index.ts | 3 - .../bot/src/components/CommandWrapper.tsx | 26 - packages/bot/src/components/DeltaCaret.tsx | 21 - packages/bot/src/components/Flag.tsx | 19 - .../GenericAllStats/components/Card.tsx | 95 --- .../GenericAllStats/components/Root.tsx | 17 - .../GenericAllStats/components/Row.tsx | 7 - .../src/components/GenericAllStats/index.tsx | 149 ----- .../components/GenericStatsColumn.tsx | 16 - .../components/GenericStatsRow.tsx | 46 -- .../bot/src/components/GenericStats/index.tsx | 34 - .../Graph/components/Line/constants.ts | 11 - .../Graph/components/Line/index.tsx | 65 -- .../Graph/components/Margin/constants.ts | 4 - .../Graph/components/Margin/index.tsx | 49 -- .../Graph/components/Root/constants.ts | 2 - .../Graph/components/Root/index.tsx | 109 --- packages/bot/src/components/Graph/index.ts | 3 - .../components/Leaderboard/components/Gap.tsx | 29 - .../Leaderboard/components/Item.tsx | 171 ----- .../Leaderboard/components/Root.tsx | 13 - .../bot/src/components/Leaderboard/index.ts | 3 - .../bot/src/components/NoData/constants.ts | 5 - packages/bot/src/components/NoData/index.tsx | 30 - .../PercentileIndicator/constants.ts | 26 - .../components/PercentileIndicator/index.tsx | 19 - .../components/Tanks/components/Column.tsx | 20 - .../Tanks/components/Item/index.tsx | 75 --- .../src/components/Tanks/components/Root.tsx | 19 - .../src/components/Tanks/components/Row.tsx | 18 - .../src/components/Tanks/components/Title.tsx | 19 - packages/bot/src/components/Tanks/index.ts | 5 - packages/bot/src/components/TierWeights.tsx | 32 - packages/bot/src/components/TitleBar.tsx | 54 -- .../bot/src/core/blitz/normalizeLeagueIcon.ts | 3 - packages/bot/src/core/blitz/resolveTankId.ts | 48 -- .../searchClansAcrossRegionsBotWrapper.ts | 23 - .../searchPlayersAcrossRegionsBotWrapper.ts | 23 - .../src/core/blitzkit/buildTechTreeLine.ts | 20 - .../src/core/blitzkit/filtersToDescription.ts | 30 - .../src/core/blitzkit/getBlitzFromDiscord.ts | 16 - .../bot/src/core/blitzkit/getPeriodNow.ts | 3 - .../bot/src/core/blitzkit/getPeriodStart.ts | 8 - packages/bot/src/core/blitzkit/iconPng.ts | 16 - packages/bot/src/core/blitzkit/jsxToPng.ts | 9 - .../bot/src/core/blitzkit/jsxToPngThreaded.ts | 12 - .../core/blitzkit/jsxToSvg/Roboto-Black.ttf | Bin 129228 -> 0 bytes .../core/blitzkit/jsxToSvg/Roboto-Bold.ttf | Bin 128676 -> 0 bytes .../bot/src/core/blitzkit/jsxToSvg/Roboto.ttf | Bin 129584 -> 0 bytes .../bot/src/core/blitzkit/jsxToSvg/index.ts | 23 - .../src/core/blitzkit/nonBlockingPromises.ts | 15 - packages/bot/src/core/blitzkit/processes.ts | 37 -- .../src/core/blitzkit/renderConfiguration.ts | 11 - .../bot/src/core/blitzkit/resolveAncestry.ts | 25 - packages/bot/src/core/blitzkit/svgToPng.ts | 5 - .../bot/src/core/blitzkit/svgToPngThreaded.ts | 46 -- packages/bot/src/core/blitzkit/tankIconPng.ts | 6 - packages/bot/src/core/blitzkit/userError.ts | 6 - .../bot/src/core/blitzstars/filterStats.ts | 89 --- .../blitzstars/getBlitzStarsLinkButton.ts | 17 - .../src/core/blitzstars/getPlayerHistories.ts | 62 -- .../bot/src/core/blitzstars/getPlayerStats.ts | 37 -- .../src/core/blitzstars/getStatsInPeriod.ts | 103 --- .../src/core/blitzstars/getTankHistories.ts | 122 ---- packages/bot/src/core/db/flagUserActivity.ts | 9 - .../bot/src/core/db/linkBlitzAndDiscord.ts | 11 - packages/bot/src/core/db/users.ts | 3 - .../bot/src/core/discord/addClanChoices.ts | 26 - .../bot/src/core/discord/addFilterOptions.ts | 126 ---- .../src/core/discord/addPeriodSubCommands.ts | 150 ----- .../core/discord/addPeriodicFilterOptions.ts | 25 - .../bot/src/core/discord/addRegionChoices.ts | 41 -- .../bot/src/core/discord/addTankChoices.ts | 25 - .../bot/src/core/discord/addTierChoices.ts | 47 -- .../src/core/discord/addUsernameChoices.ts | 24 - .../discord/autocompleteClan/constants.ts | 2 - .../core/discord/autocompleteClan/index.ts | 40 -- .../bot/src/core/discord/autocompleteTanks.ts | 57 -- .../src/core/discord/autocompleteUsername.ts | 48 -- packages/bot/src/core/discord/buttonLink.ts | 20 - .../bot/src/core/discord/buttonPrimary.ts | 20 - .../bot/src/core/discord/buttonRefresh.ts | 19 - packages/bot/src/core/discord/chunkLines.ts | 17 - packages/bot/src/core/discord/client.ts | 9 - packages/bot/src/core/discord/colors.ts | 4 - packages/bot/src/core/discord/commandToURL.ts | 27 - .../core/discord/createLocalizedCommand.ts | 125 ---- packages/bot/src/core/discord/embedInfo.ts | 12 - .../bot/src/core/discord/embedNegative.ts | 9 - .../bot/src/core/discord/embedPositive.ts | 9 - packages/bot/src/core/discord/embedWarning.ts | 9 - .../src/core/discord/getCustomPeriodParams.ts | 19 - .../src/core/discord/getFiltersFromButton.ts | 19 - .../src/core/discord/getFiltersFromCommand.ts | 24 - .../src/core/discord/getPeriodOptionName.ts | 11 - .../src/core/discord/localizationObject.ts | 35 - .../bot/src/core/discord/markdownTable.ts | 9 - .../normalizeInteractionReturnable.tsx | 64 -- packages/bot/src/core/discord/psa.ts | 36 - .../core/discord/resolveClanFromCommand.ts | 36 - .../core/discord/resolvePeriodFromButton.ts | 40 -- .../core/discord/resolvePeriodFromCommand.ts | 52 -- .../core/discord/resolvePlayerFromButton.ts | 12 - .../discord/resolvePlayerFromCommand/index.ts | 53 -- .../core/localization/strings/constants.ts | 25 - .../bot/src/core/localization/translator.ts | 20 - packages/bot/src/core/radix/radixColors.ts | 55 -- packages/bot/src/core/streams/wotbreplay.ts | 57 -- .../handlers/autocomplete.ts | 15 - .../interactionCreate/handlers/button.tsx | 27 - .../handlers/chatInputCommand.ts | 151 ----- .../bot/src/events/interactionCreate/index.ts | 135 ---- packages/bot/src/events/ready.ts | 5 - packages/bot/src/main.ts | 81 --- packages/bot/src/stitches.config.ts | 54 -- packages/bot/src/types.d.ts | 4 - packages/bot/src/types/histories.ts | 24 - packages/bot/src/types/possiblyPromise.ts | 1 - packages/bot/src/types/ttf.d.ts | 4 - packages/bot/src/types/writable.ts | 1 - packages/bot/src/workers/bot.ts | 13 - packages/bot/src/workers/render.ts | 11 - packages/bot/tsconfig.json | 4 - packages/i18n/strings/en.json | 530 --------------- packages/scripts/package.json | 1 - packages/scripts/src/buildBot.ts | 54 -- prisma/schema.prisma | 24 - 160 files changed, 8 insertions(+), 9075 deletions(-) delete mode 100644 docs/guides/invite.md delete mode 100644 packages/bot/package.json delete mode 100644 packages/bot/src/commands/about.ts delete mode 100644 packages/bot/src/commands/breakdown/index.tsx delete mode 100644 packages/bot/src/commands/breakdown/render.tsx delete mode 100644 packages/bot/src/commands/debug.ts delete mode 100644 packages/bot/src/commands/evolution.tsx delete mode 100644 packages/bot/src/commands/fullStats.tsx delete mode 100644 packages/bot/src/commands/inactive.tsx delete mode 100644 packages/bot/src/commands/link.tsx delete mode 100644 packages/bot/src/commands/ownedTanks.tsx delete mode 100644 packages/bot/src/commands/permissions.ts delete mode 100644 packages/bot/src/commands/ping.ts delete mode 100644 packages/bot/src/commands/playerInfo.ts delete mode 100644 packages/bot/src/commands/rating.tsx delete mode 100644 packages/bot/src/commands/ratingLeaderboard.tsx delete mode 100644 packages/bot/src/commands/replay.tsx delete mode 100644 packages/bot/src/commands/research.tsx delete mode 100644 packages/bot/src/commands/searchClans.ts delete mode 100644 packages/bot/src/commands/searchPlayers.ts delete mode 100644 packages/bot/src/commands/stats.tsx delete mode 100644 packages/bot/src/commands/today.ts delete mode 100644 packages/bot/src/components/AllStatsOverview/components/HeroStat/components/Glow.tsx delete mode 100644 packages/bot/src/components/AllStatsOverview/components/HeroStat/index.tsx delete mode 100644 packages/bot/src/components/AllStatsOverview/components/WN8SurroundingStat.tsx delete mode 100644 packages/bot/src/components/AllStatsOverview/index.tsx delete mode 100644 packages/bot/src/components/Breakdown/components/Root.tsx delete mode 100644 packages/bot/src/components/Breakdown/components/Row.tsx delete mode 100644 packages/bot/src/components/Breakdown/components/RowStat.tsx delete mode 100644 packages/bot/src/components/Breakdown/index.ts delete mode 100644 packages/bot/src/components/CommandWrapper.tsx delete mode 100644 packages/bot/src/components/DeltaCaret.tsx delete mode 100644 packages/bot/src/components/Flag.tsx delete mode 100644 packages/bot/src/components/GenericAllStats/components/Card.tsx delete mode 100644 packages/bot/src/components/GenericAllStats/components/Root.tsx delete mode 100644 packages/bot/src/components/GenericAllStats/components/Row.tsx delete mode 100644 packages/bot/src/components/GenericAllStats/index.tsx delete mode 100644 packages/bot/src/components/GenericStats/components/GenericStatsColumn.tsx delete mode 100644 packages/bot/src/components/GenericStats/components/GenericStatsRow.tsx delete mode 100644 packages/bot/src/components/GenericStats/index.tsx delete mode 100644 packages/bot/src/components/Graph/components/Line/constants.ts delete mode 100644 packages/bot/src/components/Graph/components/Line/index.tsx delete mode 100644 packages/bot/src/components/Graph/components/Margin/constants.ts delete mode 100644 packages/bot/src/components/Graph/components/Margin/index.tsx delete mode 100644 packages/bot/src/components/Graph/components/Root/constants.ts delete mode 100644 packages/bot/src/components/Graph/components/Root/index.tsx delete mode 100644 packages/bot/src/components/Graph/index.ts delete mode 100644 packages/bot/src/components/Leaderboard/components/Gap.tsx delete mode 100644 packages/bot/src/components/Leaderboard/components/Item.tsx delete mode 100644 packages/bot/src/components/Leaderboard/components/Root.tsx delete mode 100644 packages/bot/src/components/Leaderboard/index.ts delete mode 100644 packages/bot/src/components/NoData/constants.ts delete mode 100644 packages/bot/src/components/NoData/index.tsx delete mode 100644 packages/bot/src/components/PercentileIndicator/constants.ts delete mode 100644 packages/bot/src/components/PercentileIndicator/index.tsx delete mode 100644 packages/bot/src/components/Tanks/components/Column.tsx delete mode 100644 packages/bot/src/components/Tanks/components/Item/index.tsx delete mode 100644 packages/bot/src/components/Tanks/components/Root.tsx delete mode 100644 packages/bot/src/components/Tanks/components/Row.tsx delete mode 100644 packages/bot/src/components/Tanks/components/Title.tsx delete mode 100644 packages/bot/src/components/Tanks/index.ts delete mode 100644 packages/bot/src/components/TierWeights.tsx delete mode 100644 packages/bot/src/components/TitleBar.tsx delete mode 100644 packages/bot/src/core/blitz/normalizeLeagueIcon.ts delete mode 100644 packages/bot/src/core/blitz/resolveTankId.ts delete mode 100644 packages/bot/src/core/blitz/searchClansAcrossRegionsBotWrapper.ts delete mode 100644 packages/bot/src/core/blitz/searchPlayersAcrossRegionsBotWrapper.ts delete mode 100644 packages/bot/src/core/blitzkit/buildTechTreeLine.ts delete mode 100644 packages/bot/src/core/blitzkit/filtersToDescription.ts delete mode 100644 packages/bot/src/core/blitzkit/getBlitzFromDiscord.ts delete mode 100644 packages/bot/src/core/blitzkit/getPeriodNow.ts delete mode 100644 packages/bot/src/core/blitzkit/getPeriodStart.ts delete mode 100644 packages/bot/src/core/blitzkit/iconPng.ts delete mode 100644 packages/bot/src/core/blitzkit/jsxToPng.ts delete mode 100644 packages/bot/src/core/blitzkit/jsxToPngThreaded.ts delete mode 100644 packages/bot/src/core/blitzkit/jsxToSvg/Roboto-Black.ttf delete mode 100644 packages/bot/src/core/blitzkit/jsxToSvg/Roboto-Bold.ttf delete mode 100644 packages/bot/src/core/blitzkit/jsxToSvg/Roboto.ttf delete mode 100644 packages/bot/src/core/blitzkit/jsxToSvg/index.ts delete mode 100644 packages/bot/src/core/blitzkit/nonBlockingPromises.ts delete mode 100644 packages/bot/src/core/blitzkit/processes.ts delete mode 100644 packages/bot/src/core/blitzkit/renderConfiguration.ts delete mode 100644 packages/bot/src/core/blitzkit/resolveAncestry.ts delete mode 100644 packages/bot/src/core/blitzkit/svgToPng.ts delete mode 100644 packages/bot/src/core/blitzkit/svgToPngThreaded.ts delete mode 100644 packages/bot/src/core/blitzkit/tankIconPng.ts delete mode 100644 packages/bot/src/core/blitzkit/userError.ts delete mode 100644 packages/bot/src/core/blitzstars/filterStats.ts delete mode 100644 packages/bot/src/core/blitzstars/getBlitzStarsLinkButton.ts delete mode 100644 packages/bot/src/core/blitzstars/getPlayerHistories.ts delete mode 100644 packages/bot/src/core/blitzstars/getPlayerStats.ts delete mode 100644 packages/bot/src/core/blitzstars/getStatsInPeriod.ts delete mode 100644 packages/bot/src/core/blitzstars/getTankHistories.ts delete mode 100644 packages/bot/src/core/db/flagUserActivity.ts delete mode 100644 packages/bot/src/core/db/linkBlitzAndDiscord.ts delete mode 100644 packages/bot/src/core/db/users.ts delete mode 100644 packages/bot/src/core/discord/addClanChoices.ts delete mode 100644 packages/bot/src/core/discord/addFilterOptions.ts delete mode 100644 packages/bot/src/core/discord/addPeriodSubCommands.ts delete mode 100644 packages/bot/src/core/discord/addPeriodicFilterOptions.ts delete mode 100644 packages/bot/src/core/discord/addRegionChoices.ts delete mode 100644 packages/bot/src/core/discord/addTankChoices.ts delete mode 100644 packages/bot/src/core/discord/addTierChoices.ts delete mode 100644 packages/bot/src/core/discord/addUsernameChoices.ts delete mode 100644 packages/bot/src/core/discord/autocompleteClan/constants.ts delete mode 100644 packages/bot/src/core/discord/autocompleteClan/index.ts delete mode 100644 packages/bot/src/core/discord/autocompleteTanks.ts delete mode 100644 packages/bot/src/core/discord/autocompleteUsername.ts delete mode 100644 packages/bot/src/core/discord/buttonLink.ts delete mode 100644 packages/bot/src/core/discord/buttonPrimary.ts delete mode 100644 packages/bot/src/core/discord/buttonRefresh.ts delete mode 100644 packages/bot/src/core/discord/chunkLines.ts delete mode 100644 packages/bot/src/core/discord/client.ts delete mode 100644 packages/bot/src/core/discord/colors.ts delete mode 100644 packages/bot/src/core/discord/commandToURL.ts delete mode 100644 packages/bot/src/core/discord/createLocalizedCommand.ts delete mode 100644 packages/bot/src/core/discord/embedInfo.ts delete mode 100644 packages/bot/src/core/discord/embedNegative.ts delete mode 100644 packages/bot/src/core/discord/embedPositive.ts delete mode 100644 packages/bot/src/core/discord/embedWarning.ts delete mode 100644 packages/bot/src/core/discord/getCustomPeriodParams.ts delete mode 100644 packages/bot/src/core/discord/getFiltersFromButton.ts delete mode 100644 packages/bot/src/core/discord/getFiltersFromCommand.ts delete mode 100644 packages/bot/src/core/discord/getPeriodOptionName.ts delete mode 100644 packages/bot/src/core/discord/localizationObject.ts delete mode 100644 packages/bot/src/core/discord/markdownTable.ts delete mode 100644 packages/bot/src/core/discord/normalizeInteractionReturnable.tsx delete mode 100644 packages/bot/src/core/discord/psa.ts delete mode 100644 packages/bot/src/core/discord/resolveClanFromCommand.ts delete mode 100644 packages/bot/src/core/discord/resolvePeriodFromButton.ts delete mode 100644 packages/bot/src/core/discord/resolvePeriodFromCommand.ts delete mode 100644 packages/bot/src/core/discord/resolvePlayerFromButton.ts delete mode 100644 packages/bot/src/core/discord/resolvePlayerFromCommand/index.ts delete mode 100644 packages/bot/src/core/localization/strings/constants.ts delete mode 100644 packages/bot/src/core/localization/translator.ts delete mode 100644 packages/bot/src/core/radix/radixColors.ts delete mode 100644 packages/bot/src/core/streams/wotbreplay.ts delete mode 100644 packages/bot/src/events/interactionCreate/handlers/autocomplete.ts delete mode 100644 packages/bot/src/events/interactionCreate/handlers/button.tsx delete mode 100644 packages/bot/src/events/interactionCreate/handlers/chatInputCommand.ts delete mode 100644 packages/bot/src/events/interactionCreate/index.ts delete mode 100644 packages/bot/src/events/ready.ts delete mode 100644 packages/bot/src/main.ts delete mode 100644 packages/bot/src/stitches.config.ts delete mode 100644 packages/bot/src/types.d.ts delete mode 100644 packages/bot/src/types/histories.ts delete mode 100644 packages/bot/src/types/possiblyPromise.ts delete mode 100644 packages/bot/src/types/ttf.d.ts delete mode 100644 packages/bot/src/types/writable.ts delete mode 100644 packages/bot/src/workers/bot.ts delete mode 100644 packages/bot/src/workers/render.ts delete mode 100644 packages/bot/tsconfig.json delete mode 100644 packages/scripts/src/buildBot.ts delete mode 100644 prisma/schema.prisma diff --git a/.gitignore b/.gitignore index 9e20f4a27..2124182fd 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,6 @@ target .astro .vercel /src -/packages/bot/src/prisma dev-dist # Logs logs diff --git a/README.md b/README.md index 8e1b06629..4a7de2108 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,14 @@ If you intend to contribute to the code, note that you may need to check out sen ## Packages -| Package | Title | Source | Memo | -| ------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | -| [packages/app](packages/app) | BlitzKit | [Play Store](https://play.google.com/store/apps/details?id=com.tresabhi.blitzkit), [App Store](https://apps.apple.com/app/4321), [Microsoft Store](https://apps.microsoft.com/detail/zyxw) | The native app of BlitzKit for all platforms. Unavailable from stores at this time. | -| [packages/bot](packages/bot) | BlitzKit Discord Bot | [Discord Application Directory](https://discord.com/discovery/applications/1097673957865443370) | The one-in-all Discord bot for statistics. | -| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. | -| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. | -| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. | -| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. | -| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. | +| Package | Title | Source | Memo | +| ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| [packages/app](packages/app) | BlitzKit | [Play Store](https://play.google.com/store/apps/details?id=com.tresabhi.blitzkit), [App Store](https://apps.apple.com/app/4321), [Microsoft Store](https://apps.microsoft.com/detail/zyxw) | The native app of BlitzKit for all platforms. Unavailable from stores at this time. | +| [packages/core](packages/core) | | | Mutual internal code shared between all other packages. | +| [packages/i18n](packages/i18n) | | [Crowdin](https://crowdin.com/project/blitzkrieg) | Translations served as JSON shared across all other packages, including some utility code. | +| [packages/scripts](packages/scripts) | | | CLI utilities for the development and deployment of BlitzKit services. | +| [packages/varuna](packages/varuna) | Varuna | [NPM](https://www.npmjs.com/package/varuna) | React and Astro-oriented state management library, awaiting separation from BlitzKit. Unavailable from NPM at this time. | +| [packages/website](packages/website) | BlitzKit | https://blitzkit.app | The BlitzKit website provides a wide range of tools, APIs, and interconnects all other services. | ## Submodules @@ -40,8 +39,6 @@ If you intend to contribute to the code, note that you may need to check out sen | `PUBLIC_BRANCH` | | `main` | The branch that the website is operating on. This is completely nominal. | | `PUBLIC_GUIDE_WEBSITE` | [GuidesBlitz](https://guidesblitz.com/) | `https://guidesblitz.com` | The website that serves the encyclopedia-like content in BlitzKit. | | `SERVICING` | | `false` | Wether the website is down for servicing. | -| `DISCORD_CLIENT_ID` | [Discord Developer Portal](https://discord.com/developers/applications) | `1097673957865443370` | The BlitzKit Discord Bot's user ID. | | `GH_TOKEN` | [GitHub Fine-Grained Tokens](https://github.com/settings/personal-access-tokens) | | The GitHub API key used for checking out private submodules and pushing content to the asset repository. | -| `DISCORD_TOKEN` | [Discord Developer Portal](https://discord.com/developers/applications) | | The Discord API Key for the BlitzKit Discord Bot. | | `DATABASE_URL` | [CockroachDB](https://www.cockroachlabs.com/) | | The CockroachDB-SQL database URL used to link Wargaming and Discord users. | | `WOTB_DLC_CDN` | | | | diff --git a/docs/guides/introduction.md b/docs/guides/introduction.md index f56f1c1ae..712323555 100644 --- a/docs/guides/introduction.md +++ b/docs/guides/introduction.md @@ -2,17 +2,6 @@ 🎉 Services for everything and anything World of Tanks Blitz -## Bot - -BlitzKit is available as a Discord bot and can be [installed very easily](/guide/invite). - -- Track statistics -- Find inactive players in a clan -- Lookup owned tanks -- Create embeds for streams -- Track a player/tanks statistical evolution -- ...and a variety of other similar things - ## Website Provides a bunch of very helpful tools; https://blitzkit.app/ diff --git a/docs/guides/invite.md b/docs/guides/invite.md deleted file mode 100644 index 7a1af2ff1..000000000 --- a/docs/guides/invite.md +++ /dev/null @@ -1,3 +0,0 @@ -# Inviting the Bot - -Go to [application directory for BlitzKit](https://discord.com/application-directory/1097673957865443370) and click "Add to Server." Voila! You're ready to go. diff --git a/package.json b/package.json index a61810c71..8064764f0 100644 --- a/package.json +++ b/package.json @@ -6,15 +6,12 @@ "scripts": { "build:assets": "cd packages/scripts && bun run build:assets", "build:averages": "bun --filter=@blitzkit/scripts run build:averages", - "build:bot": "cd packages/scripts && bun run build:bot", "build:website": "cd packages/website && bun run build", "build:prisma": "prisma generate", "build:proxyclient": "cd packages/scripts && bun run build:proxyclient", "build:catalogitemaccesssor": "cd packages/scripts && bun run build:catalogitemaccesssor", "build:ratingleaderboard": "cd packages/scripts && bun run build:ratingleaderboard", "build:app": "cd packages/app && bun run build", - "dev:bot": "bun run build:bot --dev && bun start:bot --dev", - "start:bot": "bun dist/bot/main.js", "dev:docs": "vitepress dev docs", "dev:website": "cd packages/website && bun run dev", "dev:app": "cd packages/app && bun run dev", diff --git a/packages/bot/package.json b/packages/bot/package.json deleted file mode 100644 index 6e18e80ef..000000000 --- a/packages/bot/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@blitzkit/bot", - "dependencies": { - "@blitzkit/core": "workspace:*", - "@blitzkit/i18n": "workspace:*", - "@radix-ui/colors": "^3.0.0", - "@radix-ui/themes": "^3.3.0", - "@resvg/resvg-js": "^2.6.2", - "@types/adm-zip": "^0.5.7", - "@types/bun": "^1.3.9", - "@types/lodash-es": "^4.17.12", - "@types/markdown-escape": "^1.1.3", - "@types/react": "^19.2.14", - "adm-zip": "^0.5.16", - "bepaint": "^0.1.0-beta.3", - "discord.js": "^14.25.1", - "dotenv": "^17.3.1", - "fuzzysort": "^3.1.0", - "lodash-es": "^4.17.23", - "markdown-escape": "^2.0.0", - "pickleparser": "^0.2.1", - "prisma": "^6.19.2", - "protobufjs": "^7.5.4", - "react": "^19.2.4", - "satori": "^0.18.4", - "sharp": "^0.34.5" - }, - "private": true, - "scripts": { - "dev": "bun --watch src/main.ts", - "start": "bun src/main.ts" - }, - "type": "module", - "types": "src/types.d.ts" -} diff --git a/packages/bot/src/commands/about.ts b/packages/bot/src/commands/about.ts deleted file mode 100644 index 82552eca3..000000000 --- a/packages/bot/src/commands/about.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { translator } from "../core/localization/translator"; -import { - commandsArray, - type CommandRegistry, -} from "../events/interactionCreate"; - -const subcommands = [ - "embeds", - "introduction", - "invite", - "timezones", - "credits", -] as const; -type Subcommand = (typeof subcommands)[number]; - -const DOCS: Record = { - embeds: "guide/embeds", - introduction: "guide/introduction", - invite: "guide/invite", - timezones: "guide/timezones", - credits: "about/credits", -}; - -export const aboutCommand = new Promise(async (resolve) => { - resolve({ - command: createLocalizedCommand("about", [ - { subcommand: "commands" }, - ...subcommands.map((subcommand) => ({ subcommand })), - ]), - - async handler(interaction) { - const subcommand = interaction.options.getSubcommand(); - const { strings } = translator(interaction.locale); - - if (subcommand === "commands") { - return `${strings.bot.commands.about.subcommands.commands.body}${( - await Promise.all(commandsArray) - ) - .sort((a, b) => - (a.command.name_localizations?.[interaction.locale] ?? - a.command.name) < - (b.command.name_localizations?.[interaction.locale] ?? - b.command.name) - ? -1 - : 1, - ) - .map( - (registry) => - `- \`/${ - registry.command.name_localizations?.[interaction.locale] ?? - registry.command.name - }\`: ${ - registry.command.description_localizations?.[ - interaction.locale - ] ?? registry.command.description - }`, - ) - .join("\n")}`; - } - - const url = `https://raw.githubusercontent.com/tresabhi/blitzkit/main/docs/${DOCS[subcommand as Subcommand]}.md`; - const content = fetch(url).then((response) => response.text()); - - return content; - }, - }); -}); diff --git a/packages/bot/src/commands/breakdown/index.tsx b/packages/bot/src/commands/breakdown/index.tsx deleted file mode 100644 index 958e8abd2..000000000 --- a/packages/bot/src/commands/breakdown/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { addPeriodicFilterOptions } from "../../core/discord/addPeriodicFilterOptions"; -import { addUsernameChoices } from "../../core/discord/addUsernameChoices"; -import { autocompleteTanks } from "../../core/discord/autocompleteTanks"; -import { autocompleteUsername } from "../../core/discord/autocompleteUsername"; -import { createLocalizedCommand } from "../../core/discord/createLocalizedCommand"; -import { getFiltersFromButton } from "../../core/discord/getFiltersFromButton"; -import { resolvePeriodFromButton } from "../../core/discord/resolvePeriodFromButton"; -import { resolvePlayerFromButton } from "../../core/discord/resolvePlayerFromButton"; -import { translator } from "../../core/localization/translator"; -import type { CommandRegistry } from "../../events/interactionCreate"; -import { renderBreakdown } from "./render"; - -export const breakdownCommand = new Promise( - async (resolve) => { - resolve({ - command: await addPeriodicFilterOptions( - createLocalizedCommand("breakdown"), - (option) => option.addStringOption(addUsernameChoices), - ), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - return strings.bot.commands.dead; - - // const player = await resolvePlayerFromCommand(interaction); - // const period = resolvePeriodFromCommand(player.region, interaction); - // const filters = await getFiltersFromCommand(interaction); - // const path = commandToURL(interaction, { - // ...player, - // ...getCustomPeriodParams(interaction), - // ...filters, - // }); - - // return [ - // ...(await renderBreakdown( - // player, - // period, - // filters, - // interaction.locale, - // )), - // buttonRefresh(interaction, path), - // await getBlitzStarsLinkButton(player.region, player.id), - // ]; - }, - - autocomplete: (interaction) => { - autocompleteUsername(interaction); - autocompleteTanks(interaction); - }, - - async button(interaction) { - const player = await resolvePlayerFromButton(interaction); - const period = resolvePeriodFromButton(player.region, interaction); - const filters = getFiltersFromButton(interaction); - - return await renderBreakdown( - player, - period, - filters, - interaction.locale, - ); - }, - }); - }, -); diff --git a/packages/bot/src/commands/breakdown/render.tsx b/packages/bot/src/commands/breakdown/render.tsx deleted file mode 100644 index 91fda4e2b..000000000 --- a/packages/bot/src/commands/breakdown/render.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import { - type BlitzStats, - calculateWN8, - getAccountInfo, - getClanAccountInfo, - getTankStats, - getWN8Percentile, - TankDefinition, -} from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { Locale } from "discord.js"; -import { chunk } from "lodash-es"; -import * as Breakdown from "../../components/Breakdown"; -import { CommandWrapper } from "../../components/CommandWrapper"; -import { NoData } from "../../components/NoData"; -import { TitleBar } from "../../components/TitleBar"; -import { filtersToDescription } from "../../core/blitzkit/filtersToDescription"; -import { - averageDefinitions, - tankDefinitions, -} from "../../core/blitzkit/nonBlockingPromises"; -import { UserError } from "../../core/blitzkit/userError"; -import { - filterStats, - type StatFilters, -} from "../../core/blitzstars/filterStats"; -import { getStatsInPeriod } from "../../core/blitzstars/getStatsInPeriod"; -import type { ResolvedPeriod } from "../../core/discord/resolvePeriodFromCommand"; -import type { ResolvedPlayer } from "../../core/discord/resolvePlayerFromCommand"; -import { translator } from "../../core/localization/translator"; - -const ROWS_PER_PAGE = 8; -const MAX_PAGES = 9; - -export async function renderBreakdown( - { region, id }: ResolvedPlayer, - { start, end, name }: ResolvedPeriod, - filters: StatFilters, - locale: Locale, -) { - const { strings, unwrap } = translator(locale); - const awaitedTankDefinitions = await tankDefinitions; - const awaitedTankAverages = await averageDefinitions; - const statsInPeriod = await getStatsInPeriod(region, id, start, end, locale); - const { filteredOrder } = await filterStats(statsInPeriod, filters); - const accountInfo = await getAccountInfo(region, id); - const clanData = await getClanAccountInfo(region, id, ["clan"]); - const tankStats = await getTankStats(region, id); - const filterDescriptions = await filtersToDescription(filters, locale); - const orderedCurrentStats: BlitzStats[] = []; - const orderedCareerStats: BlitzStats[] = []; - const orderedCurrentWN8: (number | undefined)[] = []; - const orderedCareerWN8: (number | undefined)[] = []; - const orderedCareerWN8Full: (number | undefined)[] = []; - - if (tankStats === null) { - throw new UserError(strings.bot.common.errors.no_tank_stats); - } - - filteredOrder.forEach((id) => { - const tankAveragesEntry = awaitedTankAverages.averages[id]; - const current = statsInPeriod.diff[id]; - const career = tankStats.find((tank) => tank.tank_id === id)!.all; - const currentWN8 = tankAveragesEntry - ? calculateWN8(tankAveragesEntry.mu, current) - : undefined; - const careerWN8 = tankAveragesEntry - ? calculateWN8(tankAveragesEntry.mu, career) - : undefined; - - orderedCurrentStats.push(current); - orderedCareerStats.push(career); - orderedCurrentWN8.push(currentWN8); - orderedCareerWN8.push(careerWN8); - }); - tankStats.forEach((tank) => { - const tankAveragesEntry = awaitedTankAverages.averages[tank.tank_id]; - - orderedCareerWN8Full.push( - tankAveragesEntry - ? calculateWN8(tankAveragesEntry.mu, tank.all) - : undefined, - ); - }); - - const currentBattles = orderedCurrentStats.reduce( - (accumulator, current) => accumulator + current.battles, - 0, - ); - const careerBattles = tankStats.reduce( - (accumulator, current) => accumulator + current.all.battles, - 0, - ); - const currentWinrate = - orderedCurrentStats.reduce( - (accumulator, current) => accumulator + current.wins, - 0, - ) / currentBattles; - const careerWinrate = - tankStats.reduce( - (accumulator, current) => accumulator + current.all.wins, - 0, - ) / careerBattles; - const currentWN8 = - orderedCurrentWN8.reduce( - (accumulator, current, index) => - accumulator + (current ?? 0) * orderedCurrentStats[index].battles, - 0, - ) / - orderedCurrentStats.reduce( - (accumulator, current, index) => - accumulator + - (orderedCurrentWN8[index] === undefined ? 0 : current.battles), - 0, - ); - const careerWN8 = - orderedCareerWN8Full.reduce( - (accumulator, current) => accumulator + (current ?? 0), - 0, - ) / - tankStats.reduce( - (accumulator, current, index) => - accumulator + - (orderedCareerWN8Full[index] === undefined ? 0 : current.all.battles), - 0, - ); - const currentDamage = - orderedCurrentStats.reduce( - (accumulator, current) => accumulator + current.damage_dealt, - 0, - ) / currentBattles; - const careerDamage = - tankStats.reduce( - (accumulator, current) => accumulator + current.all.damage_dealt, - 0, - ) / careerBattles; - - const children: React.JSX.Element[] = []; - - if (filteredOrder.length > 0) { - children.push( - , - ); - - filteredOrder.forEach((id, index) => { - const tankDefinition = awaitedTankDefinitions.tanks[id] as - | TankDefinition - | undefined; - const current = orderedCurrentStats[index]; - const career = orderedCareerStats[index]; - const currentWN8 = orderedCurrentWN8[index]; - const careerWN8 = orderedCareerWN8[index]; - - children.push( - , - ); - }); - } - - const pages = chunk( - children, - Math.max(Math.ceil(children.length / MAX_PAGES), ROWS_PER_PAGE), - ); - - if (filteredOrder.length > 0) { - return pages - .map((page, index) => ( - - {index === 0 && ( - - )} - - {page} - - )) - .reverse(); - } else { - return [ - - - - - , - ]; - } -} diff --git a/packages/bot/src/commands/debug.ts b/packages/bot/src/commands/debug.ts deleted file mode 100644 index 08f43e8ba..000000000 --- a/packages/bot/src/commands/debug.ts +++ /dev/null @@ -1,37 +0,0 @@ -import packageJSON from "../../../../package.json"; -import { client } from "../core/discord/client"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; - -const executionStart = new Date().getTime(); - -export const debugCommand = new Promise((resolve) => { - resolve({ - command: createLocalizedCommand("debug"), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - const currentTime = new Date().getTime(); - const uptime = currentTime - executionStart; - const list = [ - [strings.bot.commands.debug.body.version, packageJSON.version], - [ - strings.bot.commands.debug.body.shard.label, - client.shard?.ids[0] ?? strings.bot.commands.debug.body.shard.default, - ], - [ - strings.bot.commands.debug.body.uptime, - `${Math.floor((uptime / 1000 / 60 / 60) % 24)}h ${Math.floor( - (uptime / 1000 / 60) % 60 - )}m ${Math.floor((uptime / 1000) % 60)}s ${Math.floor( - uptime % 1000 - )}ms`, - ], - ]; - return `${strings.bot.commands.debug.body.title}${list - .map(([key, value]) => `- ${key}: ${value}`) - .join("\n")}`; - }, - }); -}); diff --git a/packages/bot/src/commands/evolution.tsx b/packages/bot/src/commands/evolution.tsx deleted file mode 100644 index 9717938f9..000000000 --- a/packages/bot/src/commands/evolution.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core"; -import { Locale, SlashCommandSubcommandGroupBuilder } from "discord.js"; -import { CommandWrapper } from "../components/CommandWrapper"; -import * as Graph from "../components/Graph"; -import { LineColor } from "../components/Graph/components/Line/constants"; -import { NoData } from "../components/NoData"; -import { TitleBar } from "../components/TitleBar"; -import { getPlayerHistories } from "../core/blitzstars/getPlayerHistories"; -import { getTankHistories } from "../core/blitzstars/getTankHistories"; -import { addPeriodSubCommands } from "../core/discord/addPeriodSubCommands"; -import { addTankChoices } from "../core/discord/addTankChoices"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { autocompleteTanks } from "../core/discord/autocompleteTanks"; -import { autocompleteUsername } from "../core/discord/autocompleteUsername"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton"; -import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand"; -import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton"; -import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; - -type EvolutionStatType = "player" | "tank"; - -async function render( - { region, id }: ResolvedPlayer, - { start, end, name }: ResolvedPeriod, - type: "player" | "tank", - tankId: number | null, - locale: Locale, -) { - const accountInfo = await getAccountInfo(region, id); - const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan; - const logo = clan ? emblemURL(clan.emblem_set_id) : undefined; - const histories = await (type === "player" - ? getPlayerHistories(region, id, { - start, - end, - includeLatestHistories: true, - includePreviousHistories: true, - }) - : getTankHistories(region, id, locale, { - tankId: tankId!, - start, - end, - includeLatestHistories: true, - includePreviousHistories: true, - })); - const plot = histories.map( - (history) => - [ - history.all.battles, - (history.all.wins / history.all.battles) * 100, - ] as Graph.PlotItem, - ); - const times = histories.map((history) => history.last_battle_time); - const minTime = Math.min(...times); - const maxTime = Math.max(...times); - const ys = plot.map(([, y]) => y); - const xs = plot.map(([x]) => x); - const maxY = Math.max(...ys); - const minY = Math.min(...ys); - const maxX = Math.max(...xs); - const minX = Math.min(...xs); - - return ( - - - - {plot.length > 0 && ( - - {Graph.Line({ - color: LineColor.Red, - plot: plot, - minY, - maxY, - })} - - )} - - {plot.length === 0 && } - - ); -} - -export const evolutionCommand = new Promise( - async (resolve) => { - resolve({ - command: createLocalizedCommand("evolution", [ - { - group: "player", - modify(option: SlashCommandSubcommandGroupBuilder) { - addPeriodSubCommands(option, (option) => - option.addStringOption(addUsernameChoices), - ); - }, - }, - { - group: "tank", - modify(option: SlashCommandSubcommandGroupBuilder) { - addPeriodSubCommands(option, (option) => - option - .addStringOption(addTankChoices) - .addStringOption(addUsernameChoices), - ); - }, - }, - ]), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - return strings.bot.commands.dead; - - // const commandGroup = - // interaction.options.getSubcommandGroup() as EvolutionStatType; - // const player = await resolvePlayerFromCommand(interaction); - // const period = resolvePeriodFromCommand(player.region, interaction); - // const tankIdRaw = interaction.options.getString("tank")!; - // const tankId = - // commandGroup === "tank" - // ? await resolveTankId(tankIdRaw, interaction.locale) - // : null; - // const path = commandToURL(interaction, { - // ...player, - // ...getCustomPeriodParams(interaction), - // tankId, - // }); - - // return [ - // await render( - // player, - // period, - // commandGroup, - // tankId, - // interaction.locale - // ), - // buttonRefresh(interaction, path), - // await getBlitzStarsLinkButton( - // player.region, - // player.id, - // tankId ?? undefined - // ), - // ]; - }, - - autocomplete: (interaction) => { - autocompleteUsername(interaction); - autocompleteTanks(interaction); - }, - - async button(interaction) { - const url = new URL(`https://example.com/${interaction.customId}`); - const path = url.pathname.split("/"); - const commandGroup = path[2] as EvolutionStatType; - const player = await resolvePlayerFromButton(interaction); - const period = resolvePeriodFromButton(player.region, interaction); - - return await render( - player, - period, - commandGroup, - parseInt(url.searchParams.get("tankId") ?? "0") || null, - interaction.locale, - ); - }, - }); - }, -); diff --git a/packages/bot/src/commands/fullStats.tsx b/packages/bot/src/commands/fullStats.tsx deleted file mode 100644 index 3cc13ce3d..000000000 --- a/packages/bot/src/commands/fullStats.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { - emblemURL, - getAccountInfo, - getClanAccountInfo, - getTierWeights, -} from "@blitzkit/core"; -import { Locale } from "discord.js"; -import { CommandWrapper } from "../components/CommandWrapper"; -import { GenericAllStats } from "../components/GenericAllStats"; -import { NoData } from "../components/NoData"; -import { TierWeights } from "../components/TierWeights"; -import { TitleBar } from "../components/TitleBar"; -import { filtersToDescription } from "../core/blitzkit/filtersToDescription"; -import { filterStats, type StatFilters } from "../core/blitzstars/filterStats"; -import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod"; -import { addPeriodicFilterOptions } from "../core/discord/addPeriodicFilterOptions"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { autocompleteTanks } from "../core/discord/autocompleteTanks"; -import { autocompleteUsername } from "../core/discord/autocompleteUsername"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { getFiltersFromButton } from "../core/discord/getFiltersFromButton"; -import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton"; -import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand"; -import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton"; -import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; - -async function render( - { region, id }: ResolvedPlayer, - { start, end, name }: ResolvedPeriod, - filters: StatFilters, - locale: Locale, -) { - const { nickname } = await getAccountInfo(region, id); - const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan; - const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined; - const diffedTankStats = await getStatsInPeriod( - region, - id, - start, - end, - locale, - ); - const { stats, supplementary, filteredOrder } = await filterStats( - diffedTankStats, - filters, - ); - const filterDescriptions = await filtersToDescription(filters, locale); - const tierWeights = await getTierWeights(diffedTankStats.diff, filteredOrder); - - return ( - - - - {stats.battles === 0 && ( - - )} - {stats.battles > 0 && } - {stats.battles > 0 && ( - - )} - - ); -} - -export const fullStatsCommand = new Promise( - async (resolve) => { - resolve({ - command: await addPeriodicFilterOptions( - createLocalizedCommand("full-stats"), - (option) => option.addStringOption(addUsernameChoices), - ), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - return strings.bot.commands.dead; - - // const player = await resolvePlayerFromCommand(interaction); - // const period = resolvePeriodFromCommand(player.region, interaction); - // const filters = await getFiltersFromCommand(interaction); - // const path = commandToURL(interaction, { - // ...player, - // ...getCustomPeriodParams(interaction), - // ...filters, - // }); - - // return Promise.all([ - // render(player, period, filters, interaction.locale), - // buttonRefresh(interaction, path), - // getBlitzStarsLinkButton(player.region, player.id), - // ]); - }, - - autocomplete: (interaction) => { - autocompleteUsername(interaction); - autocompleteTanks(interaction); - }, - - async button(interaction) { - const player = await resolvePlayerFromButton(interaction); - const period = resolvePeriodFromButton(player.region, interaction); - const filters = getFiltersFromButton(interaction); - - return await render(player, period, filters, interaction.locale); - }, - }); - }, -); diff --git a/packages/bot/src/commands/inactive.tsx b/packages/bot/src/commands/inactive.tsx deleted file mode 100644 index 128384a9a..000000000 --- a/packages/bot/src/commands/inactive.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { getAccountInfo, getClanInfo } from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { Locale } from "discord.js"; -import { CommandWrapper } from "../components/CommandWrapper"; -import { GenericStats } from "../components/GenericStats"; -import { NoData } from "../components/NoData"; -import { TitleBar } from "../components/TitleBar"; -import { addClanChoices } from "../core/discord/addClanChoices"; -import { autocompleteClan } from "../core/discord/autocompleteClan"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { localizationObject } from "../core/discord/localizationObject"; -import { resolveClanFromCommand } from "../core/discord/resolveClanFromCommand"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; - -const DEFAULT_THRESHOLD = 7; - -export const inactiveCommand = new Promise((resolve) => { - resolve({ - command: createLocalizedCommand("inactive") - .addStringOption(addClanChoices) - .addNumberOption((option) => { - const { strings } = translator(Locale.EnglishUS); - - return option - .setName("threshold") - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.commands.inactive.options.threshold.name, - undefined, - true - ) - ) - .setDescription( - strings.bot.commands.inactive.options.threshold.description - ) - .setDescriptionLocalizations( - localizationObject( - (strings) => - strings.bot.commands.inactive.options.threshold.description - ) - ) - .setMinValue(0); - }), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - const { region, id } = await resolveClanFromCommand(interaction); - const threshold = - interaction.options.getNumber("threshold")! ?? DEFAULT_THRESHOLD; - const time = new Date().getTime() / 1000; - const clanInfo = await getClanInfo(region, id); - const accountInfo = await getAccountInfo(region, clanInfo.members_ids); - const inactive = accountInfo - .map( - (account) => - [ - account.nickname, - (time - account.last_battle_time) / 60 / 60 / 24, - ] as [string, number] - ) - .filter(([, inactiveDays]) => inactiveDays >= threshold) - .sort((a, b) => b[1] - a[1]) - .map( - ([name, days]) => - [ - name, - literals(strings.bot.commands.inactive.body.listing, { - days: days.toFixed(0), - }), - ] as [string, string] - ); - const hasInactiveMembers = inactive.length > 0; - - return ( - - - - {!hasInactiveMembers && ( - - )} - {hasInactiveMembers && } - - ); - }, - - autocomplete: autocompleteClan, - }); -}); diff --git a/packages/bot/src/commands/link.tsx b/packages/bot/src/commands/link.tsx deleted file mode 100644 index 3961a6687..000000000 --- a/packages/bot/src/commands/link.tsx +++ /dev/null @@ -1,193 +0,0 @@ -import { getAccountInfo, getClanAccountInfo } from '@blitzkit/core'; -import { teal } from '@radix-ui/colors'; -import { Glow } from '../components/AllStatsOverview/components/HeroStat/components/Glow'; -import { CommandWrapper } from '../components/CommandWrapper'; -import { linkBlitzAndDiscord } from '../core/db/linkBlitzAndDiscord'; -import { addUsernameChoices } from '../core/discord/addUsernameChoices'; -import { autocompleteUsername } from '../core/discord/autocompleteUsername'; -import { createLocalizedCommand } from '../core/discord/createLocalizedCommand'; -import { embedInfo } from '../core/discord/embedInfo'; -import { resolvePlayerFromCommand } from '../core/discord/resolvePlayerFromCommand'; -import { translator } from '../core/localization/translator'; -import type { CommandRegistry } from '../events/interactionCreate'; -import { theme } from '../stitches.config'; - -export const verifyCommand = new Promise((resolve) => { - resolve({ - command: createLocalizedCommand('link').addStringOption((option) => - addUsernameChoices(option).setRequired(true), - ), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - const { id, region } = await resolvePlayerFromCommand(interaction); - const discordId = BigInt(interaction.user.id); - const accountInfo = await getAccountInfo(region, id); - const clanAccountInfo = await getClanAccountInfo(region, id, ['clan']); - - await linkBlitzAndDiscord(discordId, id); - - return [ - -
- - -
-
- avatar - -
- - {interaction.user.displayName} - - - @{interaction.user.username} - -
-
- -
- accounts linked - - {strings.bot.commands.link.body.accounts_linked} - -
- -
- {clanAccountInfo?.clan ? ( - clan emblem - ) : null} -
- - {accountInfo.nickname} - - - {clanAccountInfo?.clan - ? `[${clanAccountInfo?.clan?.tag}] • ` - : ''} - {strings.common.regions.normal[region]} - -
-
-
- - -
-
, - - embedInfo( - strings.bot.commands.link.embed.title, - strings.bot.commands.link.embed.description, - ), - ]; - }, - - autocomplete: autocompleteUsername, - }); -}); diff --git a/packages/bot/src/commands/ownedTanks.tsx b/packages/bot/src/commands/ownedTanks.tsx deleted file mode 100644 index ba4b6e549..000000000 --- a/packages/bot/src/commands/ownedTanks.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { - TANK_CLASSES, - TIER_ROMAN_NUMERALS, - TankDefinition, - flags, - getAccountInfo, - getTankStats, -} from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import markdownEscape from "markdown-escape"; -import { - gameDefinitions, - tankDefinitions, -} from "../core/blitzkit/nonBlockingPromises"; -import { addTierChoices } from "../core/discord/addTierChoices"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { autocompleteUsername } from "../core/discord/autocompleteUsername"; -import { chunkLines } from "../core/discord/chunkLines"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand"; -import { translator } from "../core/localization/translator"; -import { type CommandRegistry } from "../events/interactionCreate"; - -export const ownedTanksCommand = new Promise((resolve) => { - resolve({ - command: createLocalizedCommand("owned-tanks") - .addStringOption(addTierChoices) - .addStringOption(addUsernameChoices), - - async handler(interaction) { - const { strings, unwrap } = translator(interaction.locale); - const tier = Number(interaction.options.getString("tier")); - const { id, region } = await resolvePlayerFromCommand(interaction); - const accountInfo = await getAccountInfo(region, id); - const tankStats = await getTankStats(region, id); - - if (tankStats === null) { - return strings.bot.common.errors.no_tank_stats; - } - - const filteredTanks = ( - await Promise.all( - tankStats.map(async (tankData) => ({ - tankDefinitions: (await tankDefinitions).tanks[tankData.tank_id]!, - id: tankData.tank_id, - })), - ) - ).filter((tank) => tank.tankDefinitions?.tier === tier); - const groupedTanks: Record = {}; - const nations: string[] = []; - - filteredTanks.forEach((tank) => { - if (groupedTanks[tank.tankDefinitions.nation] === undefined) { - groupedTanks[tank.tankDefinitions.nation] = [tank.tankDefinitions]; - nations.push(tank.tankDefinitions.nation); - } else { - groupedTanks[tank.tankDefinitions.nation].push(tank.tankDefinitions); - } - }); - - const awaitedTankDefinitions = await tankDefinitions; - const awaitedGameDefinitions = await gameDefinitions; - const title = literals(strings.bot.commands.owned_tanks.body.title, { - name: markdownEscape(accountInfo.nickname), - tier: TIER_ROMAN_NUMERALS[tier], - }); - const tankList = tankStats - .map(({ tank_id }) => awaitedTankDefinitions.tanks[tank_id]) - .filter((tank) => tank.tier === tier) - .sort( - (a, b) => - TANK_CLASSES.indexOf(a.class) - TANK_CLASSES.indexOf(b.class), - ) - .sort( - (a, b) => - awaitedGameDefinitions.nations.indexOf(a.nation) - - awaitedGameDefinitions.nations.indexOf(b.nation), - ) - .map( - (tank) => - `[${flags[tank.nation]} ${markdownEscape(unwrap(tank.name!))}]()`, - ); - const lines = [title, ...tankList]; - - return chunkLines(lines); - }, - - autocomplete: autocompleteUsername, - }); -}); diff --git a/packages/bot/src/commands/permissions.ts b/packages/bot/src/commands/permissions.ts deleted file mode 100644 index 1885c699e..000000000 --- a/packages/bot/src/commands/permissions.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { literals } from "@blitzkit/i18n"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { translator } from "../core/localization/translator"; -import { CommandRegistry } from "../events/interactionCreate"; - -export const permissionsCommand = new Promise((resolve) => { - resolve({ - command: createLocalizedCommand("permissions"), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - // friday the 13th permissions easter egg - const isFridayThe13th = - new Date().getDay() === 5 && new Date().getDate() === 13; - const permissions = [ - [ - interaction.appPermissions?.has("ViewChannel"), - strings.bot.commands.permissions.body.view_channels, - ], - [ - interaction.appPermissions?.has("ReadMessageHistory"), - strings.bot.commands.permissions.body.read_message_history, - ], - [ - interaction.appPermissions?.has("AttachFiles"), - strings.bot.commands.permissions.body.attach_files, - ], - [ - interaction.appPermissions?.has("UseExternalEmojis"), - strings.bot.commands.permissions.body.external_emojis, - ], - ]; - const body = permissions - .map(([hasPermission, description]) => { - if (hasPermission) { - return `${isFridayThe13th ? "😇" : "✅"} ${description}`; - } else { - return `${isFridayThe13th ? "💀" : "❌"} ${description}`; - } - }) - .join("\n"); - - return literals(strings.bot.commands.permissions.body.title, { body }); - }, - }); -}); diff --git a/packages/bot/src/commands/ping.ts b/packages/bot/src/commands/ping.ts deleted file mode 100644 index e73a09fef..000000000 --- a/packages/bot/src/commands/ping.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { literals } from "@blitzkit/i18n"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; - -export const pingCommand = new Promise((resolve) => { - resolve({ - handlesInteraction: true, - - command: createLocalizedCommand("ping", [ - { subcommand: "blitzkit" }, - { subcommand: "wotb" }, - ]), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - const subcommand = interaction.options.getSubcommand(); - const executionStart = Date.now(); - - if (subcommand === "blitzkit") { - await interaction.editReply(strings.bot.commands.ping.body.pong); - } else { - await fetch("https://api.wotblitz.com/"); - } - - const executionTime = Date.now() - executionStart; - interaction.editReply( - literals(strings.bot.commands.ping.body.pong_with_time, { - time: executionTime.toFixed(0), - }) - ); - }, - }); -}); diff --git a/packages/bot/src/commands/playerInfo.ts b/packages/bot/src/commands/playerInfo.ts deleted file mode 100644 index 2b4c61268..000000000 --- a/packages/bot/src/commands/playerInfo.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { getAccountInfo } from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import markdownEscape from "markdown-escape"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { autocompleteUsername } from "../core/discord/autocompleteUsername"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { embedInfo } from "../core/discord/embedInfo"; -import { markdownTable } from "../core/discord/markdownTable"; -import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand"; -import { translator } from "../core/localization/translator"; -import { CommandRegistry } from "../events/interactionCreate"; - -export const playerInfoCommand = new Promise((resolve) => { - resolve({ - command: - createLocalizedCommand("player-info").addStringOption(addUsernameChoices), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - const account = await resolvePlayerFromCommand(interaction); - const { id, region: region } = account; - const accountInfo = await getAccountInfo(region, id); - - return embedInfo( - literals(strings.bot.commands.player_info.body.title, { - name: markdownEscape(accountInfo.nickname), - }), - - markdownTable([ - [ - strings.bot.commands.player_info.body.nickname, - `${accountInfo.nickname}`, - ], - [ - strings.bot.commands.player_info.body.battles, - `${accountInfo.statistics.all.battles}`, - ], - [ - strings.bot.commands.player_info.body.winrate, - `${( - 100 * - (accountInfo.statistics.all.wins / - accountInfo.statistics.all.battles) - ).toFixed(2)}%`, - ], - [], - [ - strings.bot.commands.player_info.body.account_id, - `${accountInfo.account_id}`, - ], - [ - strings.bot.commands.player_info.body.created, - new Date(accountInfo.created_at * 1000).toLocaleDateString( - interaction.locale - ), - ], - [ - strings.bot.commands.player_info.body.last_battle, - new Date(accountInfo.last_battle_time * 1000).toLocaleDateString( - interaction.locale - ), - ], - ]) - ); - }, - - autocomplete: autocompleteUsername, - }); -}); diff --git a/packages/bot/src/commands/rating.tsx b/packages/bot/src/commands/rating.tsx deleted file mode 100644 index 16b1c7feb..000000000 --- a/packages/bot/src/commands/rating.tsx +++ /dev/null @@ -1,535 +0,0 @@ -import { - deltaBkrlBlitzStats, - emblemURL, - getAccountInfo, - getArchivedRatingLeaderboard, - getClanAccountInfo, - getLeagueFromScore, - getRatingInfo, - getRatingNeighbors, - imgur, -} from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { SlashCommandSubcommandBuilder } from "discord.js"; -import markdownEscape from "markdown-escape"; -import { Glow } from "../components/AllStatsOverview/components/HeroStat/components/Glow"; -import { CommandWrapper } from "../components/CommandWrapper"; -import { DeltaCaret } from "../components/DeltaCaret"; -import { TitleBar } from "../components/TitleBar"; -import { normalizeLeagueIcon } from "../core/blitz/normalizeLeagueIcon"; -import { iconPng } from "../core/blitzkit/iconPng"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; -import { theme } from "../stitches.config"; - -const LEAGUE_ACCENT = [ - "_purple", - "_sand", - "_amber", - "_mauve", - "_brown", -] as const; - -export const ratingCommand = new Promise((resolve) => { - resolve({ - command: createLocalizedCommand("rating", [ - { - subcommand: "today", - modify(option: SlashCommandSubcommandBuilder) { - option.addStringOption(addUsernameChoices); - }, - }, - { - subcommand: "season", - modify(option: SlashCommandSubcommandBuilder) { - option.addStringOption(addUsernameChoices); - }, - }, - ]), - - async handler(interaction) { - const subcommand = interaction.options.getSubcommand(true) as - | "season" - | "today"; - const { strings } = translator(interaction.locale); - const { id, region } = await resolvePlayerFromCommand(interaction); - const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan; - const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined; - - const ratingInfo = await getRatingInfo(region); - - if (ratingInfo.detail) { - throw new Error(`Rating info not available for ${region}`); - } - - const leaderboard = await getArchivedRatingLeaderboard( - region, - ratingInfo.current_season - (subcommand === "today" ? 0 : 1) - ); - - if (leaderboard.version!.$case === "v1") { - throw new Error( - "Encountered bkrl minimal format in latest season. Wait till next cron?" - ); - } - - const accountInfo = await getAccountInfo(region, id, [ - "statistics.rating", - ]); - const statsIndex = leaderboard.version!.value.entries.findIndex( - (entry) => entry.id === id - ); - const statsA = leaderboard - ? leaderboard.version!.value.entries[statsIndex!] - : null; - const positionA = leaderboard ? statsIndex! + 1 : undefined; - const statsB1 = accountInfo.statistics.rating!; - const statsB2Array = (await getRatingNeighbors(region, id, 0)).neighbors; - - if (statsB2Array === undefined) { - return literals(strings.bot.commands.rating.errors.no_participation, { - name: markdownEscape(accountInfo.nickname), - }); - } - - const [statsB2] = statsB2Array; - const league = getLeagueFromScore(statsB2.score); - - if (!statsA) { - return strings.bot.commands.rating.errors.unrecorded_stats; - } - if (!statsB1) { - return literals(strings.bot.commands.rating.errors.no_participation, { - name: markdownEscape(accountInfo.nickname), - }); - } - - const reward = ratingInfo.rewards.find( - (reward) => - statsB2.number >= reward.from_position && - statsB2.number <= reward.to_position - ); - const positionDelta = leaderboard - ? statsB2.number - positionA! - : undefined; - const delta = deltaBkrlBlitzStats( - statsA, - accountInfo.statistics.rating!, - statsB2 - ); - const leagueTheme = LEAGUE_ACCENT[league.index]; - let rewardImage = reward - ? reward.type === "vehicle" - ? reward.vehicle.preview_image_url - : reward.stuff.image_url - : undefined; - - if (rewardImage) { - const { ok } = await fetch(rewardImage); - - if (!ok) { - rewardImage = imgur("kP88vqr"); - } - } - - return ( - - - -
-
- {reward === undefined && ( - - )} - -
- {`League - -
-
- - {statsB2.score.toLocaleString(interaction.locale)} - - {delta.score !== 0 && ( - <> - - 0 - ? theme.colors.textLowContrast_green - : theme.colors.textLowContrast_red, - fontSize: 16, - }} - > - {Math.abs(delta.score).toLocaleString( - interaction.locale - )} - - - )} -
- -
- - #{statsB2.number.toLocaleString(interaction.locale)} - - {positionDelta !== undefined && positionDelta !== 0 && ( - <> - - 0 - ? theme.colors.textLowContrast_red - : theme.colors.textLowContrast_green, - fontSize: 16, - }} - > - {Math.abs(positionDelta).toLocaleString( - interaction.locale - )} - - - )} -
-
-
- - {reward && ( -
-
-
- - {reward.type === "vehicle" - ? reward.vehicle.user_string - : reward.stuff.title} - -
- -
- - Reward - {reward.type === "stuff" ? ` x${reward.count}` : ""} - -
-
- - -
- )} - - {reward === undefined && ( - - )} -
- - {/* separated because fragment middle man invalidates relative parent: https://github.com/vercel/satori/issues/605 */} - {reward && ( - - )} - {reward && ( - - )} -
- - {delta.battles > 0 && ( -
-
- - {delta.battles.toLocaleString(interaction.locale)} - - - Battles - -
-
- - {(100 * (delta.wins / delta.battles)).toFixed(0)}% - - - Winrate - -
-
- - {Math.round(delta.damage / delta.battles).toLocaleString( - interaction.locale - )} - - - Damage - -
-
- - {(100 * (delta.survived / delta.battles)).toFixed(0)}% - - - Survival - -
-
- - {(delta.kills / delta.battles).toFixed(2)} - - - Kills - -
-
- )} -
- ); - }, - }); -}); diff --git a/packages/bot/src/commands/ratingLeaderboard.tsx b/packages/bot/src/commands/ratingLeaderboard.tsx deleted file mode 100644 index 458e70d52..000000000 --- a/packages/bot/src/commands/ratingLeaderboard.tsx +++ /dev/null @@ -1,468 +0,0 @@ -import { - FIRST_MINIMAL_ARCHIVED_RATING_SEASON, - getAccountInfo, - getArchivedLatestSeasonNumber, - getArchivedRatingLeaderboard, - getClanAccountInfo, - getRatingInfo, - getRatingLeague, - getRatingNeighbors, - imgur, - isOnGoingRatingSeason, - LEAGUES, - type RatingInfo, - RatingLeaderboard, - type Region, -} from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { - type APIApplicationCommandOptionChoice, - Locale, - SlashCommandSubcommandBuilder, -} from "discord.js"; -import { range } from "lodash-es"; -import markdownEscape from "markdown-escape"; -import { CommandWrapper } from "../components/CommandWrapper"; -import * as Leaderboard from "../components/Leaderboard"; -import { TitleBar } from "../components/TitleBar"; -import { UserError } from "../core/blitzkit/userError"; -import { addRegionChoices } from "../core/discord/addRegionChoices"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { autocompleteUsername } from "../core/discord/autocompleteUsername"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { localizationObject } from "../core/discord/localizationObject"; -import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; - -interface SimplifiedPlayer { - id: number; - score: number; - position: number; - clan: string | undefined; - nickname: string; -} - -const DEFAULT_LIMIT = 10; - -export const ratingLeaderboardCommand = new Promise( - async (resolve) => { - const latestArchivedSeasonNumber = await getArchivedLatestSeasonNumber(); - const onGoingSeason = await isOnGoingRatingSeason(); - const seasonNumbers = range( - FIRST_MINIMAL_ARCHIVED_RATING_SEASON, - latestArchivedSeasonNumber + (onGoingSeason ? 1 : 0) + 1 - ).slice(-25); - - function addOptions(option: SlashCommandSubcommandBuilder) { - const { strings } = translator(Locale.EnglishUS); - - return option - .addIntegerOption((option) => - option - .setName(strings.bot.commands.rating_leaderboard.options.limit.name) - .setNameLocalizations( - localizationObject( - (strings) => - strings.bot.commands.rating_leaderboard.options.limit.name, - undefined, - true - ) - ) - .setDescription( - literals( - strings.bot.commands.rating_leaderboard.options.limit - .description, - { count: DEFAULT_LIMIT } - ) - ) - .setDescriptionLocalizations( - localizationObject( - (strings) => - strings.bot.commands.rating_leaderboard.options.limit - .description, - { count: DEFAULT_LIMIT } - ) - ) - .setRequired(false) - .setMinValue(5) - .setMaxValue(30) - ) - .addStringOption((option) => - option - .setName( - strings.bot.commands.rating_leaderboard.options.season.name - ) - .setNameLocalizations( - localizationObject( - (strings) => - strings.bot.commands.rating_leaderboard.options.season.name, - undefined, - true - ) - ) - .setDescription( - strings.bot.commands.rating_leaderboard.options.season.description - ) - .setDescriptionLocalizations( - localizationObject( - (strings) => - strings.bot.commands.rating_leaderboard.options.season - .description - ) - ) - .setRequired(false) - .addChoices( - ...seasonNumbers - .map((number, index) => { - const current = - index === seasonNumbers.length - 1 && onGoingSeason; - - return { - name: current - ? literals( - strings.bot.commands.rating_leaderboard.options.season - .choices.current, - { season: number } - ) - : `${number}`, - value: `${number}`, - name_localizations: current - ? localizationObject( - (strings) => - strings.bot.commands.rating_leaderboard.options - .season.choices.current, - { season: number } - ) - : undefined, - } satisfies APIApplicationCommandOptionChoice; - }) - .reverse() - ) - ); - } - - resolve({ - command: createLocalizedCommand("rating-leaderboard", [ - { - subcommand: "neighbors", - modify(option: SlashCommandSubcommandBuilder) { - option.addStringOption(addUsernameChoices); - addOptions(option); - }, - }, - { - subcommand: "league", - modify(option: SlashCommandSubcommandBuilder) { - option - .addStringOption((option) => { - const { strings } = translator(Locale.EnglishUS); - - return option - .setName( - strings.bot.commands.rating_leaderboard.options.league.name - ) - .setNameLocalizations( - localizationObject( - (strings) => - strings.bot.commands.rating_leaderboard.options.league - .name, - undefined, - true - ) - ) - .setDescription( - strings.bot.commands.rating_leaderboard.options.league - .description - ) - .setDescriptionLocalizations( - localizationObject( - (strings) => - strings.bot.commands.rating_leaderboard.options.league - .description - ) - ) - .addChoices( - ...LEAGUES.map( - ({ name }, value) => - ({ - name: ( - strings.common.leagues as Record - )[name], - value: `${value}`, - name_localizations: localizationObject( - (strings) => - ( - strings.common.leagues as Record - )[name] - ), - } satisfies APIApplicationCommandOptionChoice) - ) - ) - .setRequired(true); - }) - .addStringOption(addRegionChoices); - addOptions(option); - }, - }, - ]), - - // BIG TODO: title seems to be in english - - async handler(interaction) { - const { strings } = translator(interaction.locale); - const subcommand = interaction.options.getSubcommand(true) as - | "league" - | "neighbors"; - const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT; - const seasonOption = interaction.options.getString("season"); - const season = seasonOption - ? parseInt(seasonOption) - : onGoingSeason - ? latestArchivedSeasonNumber + 1 - : latestArchivedSeasonNumber; - const isCurrentSeason = season === latestArchivedSeasonNumber + 1; - let titleName: string; - let titleImage: string | undefined; - let titleDescription: string; - let playersAfter: number; - let players: SimplifiedPlayer[] | undefined; - let archivedLeaderboard: RatingLeaderboard | null; - let playerId: number | undefined; - let regionRatingInfo: RatingInfo | null; - - if (subcommand === "league") { - const leagueIndex = parseInt( - interaction.options.getString("league")! - ); - const region = interaction.options.getString("region") as Region; - regionRatingInfo = await (isCurrentSeason - ? getRatingInfo(region) - : null); - - if ( - regionRatingInfo !== null && - regionRatingInfo.detail !== undefined - ) { - return strings.bot.commands.rating_leaderboard.body - .no_ongoing_season; - } - - const result = isCurrentSeason - ? await getRatingLeague(region, leagueIndex).then(async (result) => - 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) - ) - ) - : await getArchivedRatingLeaderboard(region, season!).then( - async (players) => { - const firstPlayerIndex = - leagueIndex === 0 - ? 0 - : players.version!.value.entries.findIndex( - (player) => - player.score < LEAGUES[leagueIndex - 1].minScore - ); - const lastPlayerIndex = firstPlayerIndex + limit - 1; - const trimmed = players.version!.value.entries.slice( - firstPlayerIndex, - lastPlayerIndex + 1 - ); - const clanData = await getClanAccountInfo( - region, - trimmed.map(({ id }) => id), - ["clan"] - ); - - return trimmed.map( - (player, index) => - ({ - id: player.id, - score: player.score, - position: firstPlayerIndex + index + 1, - clan: clanData[player.id]?.clan?.tag, - nickname: - clanData[index]?.account_name ?? - literals( - strings.bot.commands.rating_leaderboard.body - .deleted_player, - { id: player.id } - ), - } satisfies SimplifiedPlayer) - ); - } - ); - const leagueInfo = LEAGUES[leagueIndex]; - archivedLeaderboard = await getArchivedRatingLeaderboard( - region, - season - ); - - players = result; - playersAfter = - (regionRatingInfo === null - ? archivedLeaderboard.version!.value.entries.length - : regionRatingInfo.count) - result[result.length - 1].position; - titleName = `${ - leagueInfo - ? (strings.common.leagues as Record)[ - LEAGUES[leagueInfo.index].name - ] - : "" - } - ${strings.common.regions.short[region]}`; - titleImage = imgur(leagueInfo.icon); - titleDescription = literals( - strings.bot.commands.rating_leaderboard.body.top_players, - { count: limit } - ); - } else { - const { region, id } = await resolvePlayerFromCommand(interaction); - regionRatingInfo = isCurrentSeason - ? await getRatingInfo(region) - : null; - - if ( - regionRatingInfo !== null && - regionRatingInfo.detail !== undefined - ) { - return strings.bot.commands.rating_leaderboard.body - .no_ongoing_season; - } - - const accountInfo = await getAccountInfo(region, id); - const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan; - const neighbors = isCurrentSeason - ? await getRatingNeighbors(region, id, Math.round(limit / 2)).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) - ) - ) - : await getArchivedRatingLeaderboard(region, season!).then( - async (players) => { - const playerIndex = players.version!.value.entries.findIndex( - (player) => player.id === id - ); - - if (playerIndex === -1) { - throw new UserError( - literals( - strings.bot.commands.rating_leaderboard.body - .no_participation, - { player: markdownEscape(accountInfo.nickname), season } - ) - ); - } - - const halfRange = Math.round(limit / 2); - const trimmed = players.version!.value.entries.slice( - playerIndex - halfRange, - playerIndex + halfRange + 1 - ); - const clanData = await getClanAccountInfo( - region, - trimmed.map((player) => player.id), - ["clan"] - ); - - return trimmed.map((player, index) => ({ - id: player.id, - score: player.score, - position: playerIndex + index + 1, - clan: clanData[index]?.clan?.tag, - nickname: clanData[index] - ? clanData[index]!.account_name - : `Deleted ${player.id}`, - })); - } - ); - archivedLeaderboard = await getArchivedRatingLeaderboard( - region, - season - ); - - players = neighbors; - titleImage = clan - ? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clan.emblem_set_id}.png` - : undefined; - titleName = neighbors ? accountInfo.nickname : ""; - titleDescription = - strings.bot.commands.rating_leaderboard.body.neighbors; - playersAfter = - (regionRatingInfo === null - ? archivedLeaderboard.version!.value.entries.length - : regionRatingInfo.count) - - neighbors[neighbors.length - 1].position; - playerId = id; - } - - const items = players.map((player) => { - const midnightIndex = - archivedLeaderboard?.version!.value.entries.findIndex( - (item) => item.id === player.id - ); - const midnightScore = - midnightIndex === undefined - ? player.score - : archivedLeaderboard!.version!.value.entries[midnightIndex] - .score; - const midnightPosition = - midnightIndex === undefined ? player.position : midnightIndex + 1; - - return ( - - ); - }); - - return ( - - - - {items && ( - - {items} - {playersAfter > 0 && ( - - )} - - )} - - ); - }, - - autocomplete: autocompleteUsername, - }); - } -); diff --git a/packages/bot/src/commands/replay.tsx b/packages/bot/src/commands/replay.tsx deleted file mode 100644 index ed6da65fc..000000000 --- a/packages/bot/src/commands/replay.tsx +++ /dev/null @@ -1,592 +0,0 @@ -import { - calculateWN8, - emblemURL, - getClanAccountInfo, - idToRegion, - tankIcon, - TIER_ROMAN_NUMERALS, -} from "@blitzkit/core"; -import { Locale } from "discord.js"; -import { clamp } from "lodash-es"; -import { CommandWrapper } from "../components/CommandWrapper"; -import { TitleBar } from "../components/TitleBar"; -import { iconPng } from "../core/blitzkit/iconPng"; -import { - averageDefinitions, - mapDefinitions, - tankDefinitions, -} from "../core/blitzkit/nonBlockingPromises"; -import { RenderConfiguration } from "../core/blitzkit/renderConfiguration"; -import { buttonLink } from "../core/discord/buttonLink"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { localizationObject } from "../core/discord/localizationObject"; -import { translator } from "../core/localization/translator"; -import { type CommandRegistry } from "../events/interactionCreate"; -import { theme } from "../stitches.config"; - -type WotInspectorReplaySubmission = - | { status: "error"; data: unknown; error: { message: string } } - | { - status: "ok"; - data: { - download_url: string; - view_url: string; - summary: { - battle_start_time: string; - map_name: string; - battle_start_timestamp: number; - battle_duration: number; - title: string; - arena_unique_id: string; - allies: number[]; - enemies: number[]; - protagonist: number; - player_name: string; - vehicle: string; - exp_base: number; - exp_total: number; - credits_base: number; - credits_total: number; - room_type: number; - battle_type: number; - mastery_badge: number; - battle_result: number; - vehicle_tier: number; - vehicle_type: number; - protagonist_team: number; - winner_team: number; - details: { - achievements: unknown[]; - base_capture_points: number; - base_defend_points: number; - chassis_id: unknown | null; - clan_tag: string; - clanid: number; - credits: number; - damage_assisted: number; - damage_assisted_track: number; - damage_blocked: unknown | null; - damage_made: number; - damage_received: number; - dbid: number; - death_reason: number; - distance_travelled: number; - enemies_damaged: number; - enemies_destroyed: number; - enemies_spotted: number; - entity_id: number; - exp: number; - exp_for_assist: number; - exp_for_damage: number; - exp_team_bonus: number; - gun_id: unknown | null; - hero_bonus_credits: number; - hero_bonus_exp: number; - hitpoints_left: number; - hits_bounced: number; - hits_pen: number; - hits_received: number; - hits_splash: number; - killed_by: number; - name: string; - shots_hit: number; - shots_made: number; - shots_pen: number; - shots_splash: number; - squad_index: number; - team: number; - time_alive: number; - turret_id: unknown | null; - vehicle_descr: number; - wp_points_earned: number; - wp_points_stolen: number; - }; - }; - }; - error: {}; - }; - -interface WotInspectorReplay { - id: string; - map_id: number; - battle_duration: number; - title: unknown | null; - player_name: string; - protagonist: number; - vehicle_descr: number; - mastery_badge: number; - exp_base: number; - enemies_spotted: number; - enemies_destroyed: number; - damage_assisted: number; - damage_made: number; - details_url: string; - download_url: string; - game_version: { - name: string; - package: string; - }; - arena_unique_id: string; - download_count: number; - data_version: number; - private: boolean; - private_clan: boolean; - battle_start_time: Date; - upload_time: Date; - allies: number[]; - enemies: number[]; - protagonist_clan: number; - protagonist_team: number; - battle_result: number; - credits_base: number; - tags: number[]; - battle_type: number; - room_type: number; - last_accessed_time: Date; - winner_team: number; - finish_reason: number; - players_data: WotInspectorReplayPlayerData[]; - exp_total: number; - credits_total: number; - repair_cost: number; - exp_free: number; - exp_free_base: number; - exp_penalty: number; - credits_penalty: number; - credits_contribution_in: number; - credits_contribution_out: number; - camouflage_id: number; -} - -export interface WotInspectorReplayPlayerData { - team: number; - name: string; - entity_id: number; - dbid: number; - clanid: number | null; - clan_tag: null | string; - hitpoints_left: number; - credits: number; - exp: number; - shots_made: number; - shots_hit: number; - shots_splash: number; - shots_pen: number; - damage_made: number; - damage_received: number; - damage_assisted: number; - damage_assisted_track: number; - hits_received: number; - hits_bounced: number; - hits_splash: number; - hits_pen: number; - enemies_spotted: number; - enemies_damaged: number; - enemies_destroyed: number; - time_alive: number; - distance_travelled: number; - killed_by: number; - base_capture_points: number; - base_defend_points: number; - exp_for_damage: number; - exp_for_assist: number; - exp_team_bonus: number; - wp_points_earned: number; - wp_points_stolen: number; - hero_bonus_credits: number; - hero_bonus_exp: number; - death_reason: number; - achievements: any[]; - vehicle_descr: number; - turret_id: number; - gun_id: number; - chassis_id: number; - squad_index: number; - damage_blocked: number; -} - -function stat(value: string | number, flex = 2) { - return ( -
- {value} -
- ); -} - -async function playerListing( - locale: Locale, - player: WotInspectorReplayPlayerData, - protagonistTeam: boolean, - winningTeam: number, - recordingPlayer = false, -) { - const { unwrap } = translator(locale); - const awaitedTankAverages = await averageDefinitions; - const awaitedTankDefinitions = await tankDefinitions; - const tank = awaitedTankDefinitions.tanks[player.vehicle_descr]; - const blockAccent = recordingPlayer ? "_amber" : ""; - const healthAccent = protagonistTeam - ? recordingPlayer - ? "_amber" - : "_green" - : "_red"; - - // our turret ids are always off by 1 or 2 lol so imma just grab the closest - // TODO: 💀 - const turret = tank.turrets.sort( - (a, b) => - Math.abs(a.id - player.turret_id) - Math.abs(b.id - player.turret_id), - )[0]; - const totalHealth = turret.health + tank.health; - const healthLeft = clamp(player.hitpoints_left / totalHealth, 0, 1); - const tankAveragesEntry = awaitedTankAverages.averages[player.vehicle_descr]; - const wn8 = - tankAveragesEntry === undefined - ? -1 - : calculateWN8(tankAveragesEntry.mu, { - battles: 1, - damage_dealt: player.damage_made, - dropped_capture_points: player.base_defend_points, - frags: player.enemies_destroyed, - spotted: player.enemies_spotted, - wins: winningTeam === player.team ? 1 : 0, - }); - - return ( -
-
- {unwrap(tank.name!)} - -
-
- - {TIER_ROMAN_NUMERALS[tank.tier]} - - - {unwrap(tank.name!)} - -
- - - {player.name} - {player.clan_tag ? ` [${player.clan_tag}]` : ""} - -
-
- -
- {stat(player.damage_made.toLocaleString())} - {stat(wn8 === -1 ? "--" : Math.round(wn8).toLocaleString())} - {stat(player.exp.toLocaleString())} - {stat(player.enemies_destroyed, 1)} -
- -
-
-
-
- ); -} - -function Header() { - return ( -
-
- -
-
- - Damage - -
-
- - WN8 - -
-
- - XP - -
-
- - Kills - -
-
-
- ); -} - -export const replayCommand = new Promise((resolve) => { - const { strings } = translator(Locale.EnglishUS); - - resolve({ - command: createLocalizedCommand("replay").addAttachmentOption((option) => - option - .setName(strings.bot.commands.replay.options.file.name) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.commands.replay.options.file.name, - undefined, - true, - ), - ) - .setDescription(strings.bot.commands.replay.options.file.description) - .setDescriptionLocalizations( - localizationObject( - (strings) => strings.bot.commands.replay.options.file.description, - ), - ) - .setRequired(true), - ), - - async handler(interaction) { - const awaitedMapDefinitions = await mapDefinitions; - const file = interaction.options.getAttachment("file")!; - const submissionData = (await fetch( - `https://wotinspector.com/api/replay/upload/?url=${encodeURIComponent( - file.url, - )}`, - ).then((response) => response.json())) as WotInspectorReplaySubmission; - - if (submissionData.status === "error") { - return `# Uh oh! WoT Inspector didn't like that.\n\nThere was an error processing your replay. Get help on [the official Discord server](https://discord.gg/nDt7AjGJQH).`; - } - - const replayId = submissionData.data.download_url.split("/").at(-1)!; - const replayData = (await fetch( - `https://api.wotinspector.com/v2/blitz/replays/${replayId}/`, - ).then((response) => response.json())) as WotInspectorReplay; - const antagonistTeamId = 3 - replayData.protagonist_team; - const players = replayData.players_data.sort( - (a, b) => b.damage_made - a.damage_made, - ); - const clan = ( - await getClanAccountInfo( - idToRegion(replayData.protagonist), - replayData.protagonist, - ["clan"], - ) - )?.clan; - const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined; - - return [ - new RenderConfiguration({ width: 960 }), - - - -
-
-
- - {await Promise.all( - players - .filter( - (player) => player.team === replayData.protagonist_team, - ) - .map((player) => - playerListing( - interaction.locale, - player, - true, - replayData.winner_team, - player.dbid === replayData.protagonist, - ), - ), - )} -
- -
-
- - {await Promise.all( - players - .filter((player) => player.team === antagonistTeamId) - .map((player) => - playerListing( - interaction.locale, - player, - false, - replayData.winner_team, - ), - ), - )} -
-
-
, - buttonLink(submissionData.data.view_url, "WoT Inspector"), - buttonLink(submissionData.data.download_url, "Download"), - ]; - }, - }); -}); diff --git a/packages/bot/src/commands/research.tsx b/packages/bot/src/commands/research.tsx deleted file mode 100644 index e0a987044..000000000 --- a/packages/bot/src/commands/research.tsx +++ /dev/null @@ -1,626 +0,0 @@ -import { - asset, - emblemURL, - equipmentPriceMatrix, - getAccountInfo, - getClanAccountInfo, - getTankStats, - TANK_ICONS, - TankType, - TIER_ROMAN_NUMERALS, -} from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { escapeMarkdown, Locale } from "discord.js"; -import { CommandWrapper } from "../components/CommandWrapper"; -import { TitleBar } from "../components/TitleBar"; -import { resolveTankId } from "../core/blitz/resolveTankId"; -import { buildTechTreeLine } from "../core/blitzkit/buildTechTreeLine"; -import { iconPng } from "../core/blitzkit/iconPng"; -import { tankDefinitions } from "../core/blitzkit/nonBlockingPromises"; -import { resolveAncestry } from "../core/blitzkit/resolveAncestry"; -import { tankIconPng } from "../core/blitzkit/tankIconPng"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { autocompleteTanks } from "../core/discord/autocompleteTanks"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { localizationObject } from "../core/discord/localizationObject"; -import { resolvePlayerFromCommand } from "../core/discord/resolvePlayerFromCommand"; -import { translator } from "../core/localization/translator"; -import { type CommandRegistry } from "../events/interactionCreate"; -import { theme } from "../stitches.config"; - -export const researchCommand = new Promise((resolve) => { - const { strings } = translator(Locale.EnglishUS); - - resolve({ - command: createLocalizedCommand("research") - .addStringOption((option) => - option - .setName(strings.bot.commands.research.options.target_tank.name!) - .setNameLocalizations( - localizationObject( - (strings) => - strings.bot.commands.research.options.target_tank.name, - undefined, - true, - ), - ) - .setDescription( - strings.bot.commands.research.options.target_tank.description, - ) - .setDescriptionLocalizations( - localizationObject( - (strings) => - strings.bot.commands.research.options.target_tank.description, - ), - ) - .setAutocomplete(true) - .setRequired(true), - ) - .addStringOption((option) => - option - .setName(strings.bot.commands.research.options.starting_tank.name!) - .setNameLocalizations( - localizationObject( - (strings) => - strings.bot.commands.research.options.starting_tank.name, - undefined, - true, - ), - ) - .setDescription( - strings.bot.commands.research.options.starting_tank.description, - ) - .setDescriptionLocalizations( - localizationObject( - (strings) => - strings.bot.commands.research.options.starting_tank.description, - ), - ) - .setAutocomplete(true) - .setRequired(false), - ) - .addStringOption(addUsernameChoices), - - async handler(interaction) { - const { strings, unwrap } = translator(interaction.locale); - const awaitedTankDefinitions = await tankDefinitions; - const targetTankId = await resolveTankId( - interaction.options.getString("target-tank", true), - interaction.locale, - true, - ); - const startingTankRaw = interaction.options.getString("starting-tank"); - const targetTank = awaitedTankDefinitions.tanks[targetTankId]; - const { id, region } = await resolvePlayerFromCommand(interaction); - let startingTankId: number; - const targetTankAncestry = await resolveAncestry(targetTankId); - - if (startingTankRaw) { - startingTankId = await resolveTankId( - startingTankRaw, - interaction.locale, - true, - ); - - if (targetTankId === startingTankId) { - return literals( - strings.bot.commands.research.errors.start_end_equal, - { tank: escapeMarkdown(unwrap(targettank.name!)) }, - ); - } - - const startingTank = awaitedTankDefinitions.tanks[startingTankId]; - - if (startingTank.tier > targetTank.tier) { - return literals(strings.bot.commands.research.errors.unordered, { - tank_1: escapeMarkdown(unwrap(startingtank.name!)), - tank_2: escapeMarkdown(unwrap(targettank.name!)), - }); - } - - if (!targetTankAncestry.includes(startingTankId)) { - return literals( - strings.bot.commands.research.errors.tanks_not_on_line, - { - tank_1: escapeMarkdown(unwrap(targettank.name!)), - tank_2: escapeMarkdown(unwrap(startingtank.name!)), - }, - ); - } - } else { - if (targetTank.type !== TankType.TANK_TYPE_RESEARCHABLE) { - return literals(strings.bot.commands.research.errors.non_tech_tree, { - tank: escapeMarkdown(unwrap(targettank.name!)), - }); - } - - if (targetTank.tier === 1) { - return literals(strings.bot.commands.research.errors.tier_1, { - tank: escapeMarkdown(unwrap(targettank.name!)), - }); - } - - const tankStats = await getTankStats(region, id); - - if (tankStats === null) { - return strings.bot.common.errors.no_tank_stats; - } - - if (tankStats.some(({ tank_id }) => tank_id === targetTankId)) { - return literals( - strings.bot.commands.research.errors.already_researched, - { tank: escapeMarkdown(unwrap(targettank.name!)) }, - ); - } - - const foundAncestor = targetTankAncestry.find( - (id) => - awaitedTankDefinitions.tanks[id].tier === 1 || - tankStats.some(({ tank_id }) => tank_id === id), - ); - - if (foundAncestor === undefined) { - throw new Error("No first owned tech tree found."); - } - - startingTankId = foundAncestor; - } - - const techTreeLine = await buildTechTreeLine( - startingTankId, - targetTankId, - ); - - const line = [...techTreeLine, startingTankId].reverse(); - const { nickname } = await getAccountInfo(region, id); - const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan; - const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined; - const costs = await Promise.all( - line.map(async (id) => { - const tank = awaitedTankDefinitions.tanks[id]; - const turretXps = new Map(); - const gunXps = new Map(); - const engineXps = new Map(); - const trackXps = new Map(); - - tank.turrets.forEach((turret) => { - if ( - turret.research_cost !== undefined && - typeof turret.research_cost.research_cost_type!.value === "number" - ) { - turretXps.set( - turret.id, - turret.research_cost.research_cost_type!.value, - ); - } - - turret.guns.forEach((gun) => { - if ( - gun.research_cost !== undefined && - typeof gun.research_cost.research_cost_type!.value === "number" - ) { - gunXps.set(gun.id, gun.research_cost.research_cost_type!.value); - } - }); - }); - - tank.engines.forEach((engine) => { - if ( - engine.research_cost !== undefined && - typeof engine.research_cost.research_cost_type!.value === "number" - ) { - engineXps.set( - engine.id, - engine.research_cost.research_cost_type!.value, - ); - } - }); - - tank.tracks.forEach((track) => { - if ( - track.research_cost !== undefined && - typeof track.research_cost.research_cost_type!.value === "number" - ) { - trackXps.set( - track.id, - track.research_cost.research_cost_type!.value, - ); - } - }); - - let moduleXp = 0; - turretXps.forEach((value) => { - moduleXp += value; - }); - gunXps.forEach((value) => { - moduleXp += value; - }); - engineXps.forEach((value) => { - moduleXp += value; - }); - trackXps.forEach((value) => { - moduleXp += value; - }); - - return { - research: tank.research_cost!.research_cost_type!.value as number, - upgrades: moduleXp, - purchase: tank.price!.value, - equipment: equipmentPriceMatrix[tank.tier].reduce( - (a, b) => a + 3 * b, - 0, - ), - }; - }), - ); - - const image = ( - - - -
-
-
- -
- - {strings.bot.commands.research.body.total} - -
- -
-
- XP icon - - Experience:{" "} - {costs - .reduce((a, b) => a + (b.research ?? 0) + b.upgrades, 0) - .toLocaleString(interaction.locale)} - -
- -
- Silver icon - - Purchase:{" "} - {costs - .reduce((a, b) => a + b.purchase, 0) - .toLocaleString(interaction.locale)} - -
- -
- Silver icon - - Equipment:{" "} - {costs - .reduce((a, b) => a + b.equipment, 0) - .toLocaleString(interaction.locale)} - -
-
-
- - {await Promise.all( - line.map(async (id, index) => { - const tank = awaitedTankDefinitions.tanks[id]; - - return ( -
-
- - {TIER_ROMAN_NUMERALS[tank.tier]} - -
- -
- Tank icon - -
- {unwrap(tank.name!)} - - - {unwrap(tank.name!)} - -
-
- -
-
- XP icon - - {literals( - strings.bot.commands.research.body.research, - { - cost: costs[index].research?.toLocaleString( - interaction.locale, - ), - }, - )} - -
- - {costs[index].upgrades > 0 && ( -
- Free XP icon - - {literals( - strings.bot.commands.research.body.upgrades, - { cost: costs[index].upgrades.toLocaleString() }, - )} - -
- )} - -
- Silver icon - - {literals( - strings.bot.commands.research.body.purchase, - { - cost: costs[index].purchase.toLocaleString( - interaction.locale, - ), - }, - )} - -
- -
- Silver icon - - {literals( - strings.bot.commands.research.body.equipment, - { - cost: costs[index].equipment.toLocaleString( - interaction.locale, - ), - }, - )} - -
-
-
- ); - }), - )} -
-
- ); - - return startingTankRaw - ? image - : [strings.bot.commands.research.body.estimation, image]; - }, - - autocomplete(interaction) { - autocompleteTanks(interaction, true, ["target-tank", "starting-tank"]); - }, - }); -}); diff --git a/packages/bot/src/commands/searchClans.ts b/packages/bot/src/commands/searchClans.ts deleted file mode 100644 index 45c0c6aee..000000000 --- a/packages/bot/src/commands/searchClans.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { type ClanList, fetchBlitz, type Region } from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { Locale } from "discord.js"; -import markdownEscape from "markdown-escape"; -import { addClanChoices } from "../core/discord/addClanChoices"; -import { addRegionChoices } from "../core/discord/addRegionChoices"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { embedInfo } from "../core/discord/embedInfo"; -import { localizationObject } from "../core/discord/localizationObject"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; - -const DEFAULT_LIMIT = 25; - -export const searchClansCommand = new Promise((resolve) => { - resolve({ - command: createLocalizedCommand("search-clans") - .addStringOption(addRegionChoices) - .addStringOption((option) => - addClanChoices(option).setAutocomplete(false) - ) - .addIntegerOption((option) => { - const { strings } = translator(Locale.EnglishUS); - - return option - .setName(strings.bot.commands.search_clans.options.limit.name) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.commands.search_clans.options.limit.name, - undefined, - true - ) - ) - .setDescription( - literals( - strings.bot.commands.search_clans.options.limit.description, - { limit: DEFAULT_LIMIT } - ) - ) - .setDescriptionLocalizations( - localizationObject( - (strings) => - strings.bot.commands.search_clans.options.limit.description, - { limit: DEFAULT_LIMIT } - ) - ) - .setMinValue(1) - .setMaxValue(100); - }), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - const region = interaction.options.getString("region") as Region; - const clan = interaction.options.getString("clan")!; - const limit = interaction.options.getInteger("limit") ?? DEFAULT_LIMIT; - const clanList = await fetchBlitz(region, "clans/list", { - search: clan, - limit, - }); - - return embedInfo( - literals(strings.bot.commands.search_clans.body.title, { - search: markdownEscape(clan), - region: strings.common.regions.normal[region], - }), - clanList.length === 0 - ? strings.bot.commands.search_clans.body.no_results - : `\`\`\`\n${clanList - .map((clan) => `${clan.name} [${clan.tag}]`) - .join("\n")}\n\`\`\`` - ); - }, - }); -}); diff --git a/packages/bot/src/commands/searchPlayers.ts b/packages/bot/src/commands/searchPlayers.ts deleted file mode 100644 index 54bfa4dc1..000000000 --- a/packages/bot/src/commands/searchPlayers.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - type AccountList, - fetchBlitz, - type Region, - usernamePattern, -} from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { Locale } from "discord.js"; -import markdownEscape from "markdown-escape"; -import { addRegionChoices } from "../core/discord/addRegionChoices"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { embedInfo } from "../core/discord/embedInfo"; -import { localizationObject } from "../core/discord/localizationObject"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; - -const DEFAULT_LIMIT = 25; - -export const searchPlayersCommand = new Promise((resolve) => { - const { strings } = translator(Locale.EnglishUS); - - resolve({ - command: createLocalizedCommand("search-players") - .addStringOption(addRegionChoices) - .addStringOption((option) => - addUsernameChoices(option).setAutocomplete(false).setRequired(true) - ) - .addIntegerOption((option) => - option - .setName(strings.bot.commands.search_players.options.limit.name) - .setNameLocalizations( - localizationObject( - (strings) => - strings.bot.commands.search_players.options.limit.name, - undefined, - true - ) - ) - .setDescription( - literals( - strings.bot.commands.search_players.options.limit.description, - { limit: DEFAULT_LIMIT } - ) - ) - .setDescriptionLocalizations( - localizationObject( - (strings) => - strings.bot.commands.search_players.options.limit.description, - { limit: DEFAULT_LIMIT } - ) - ) - .setMinValue(1) - .setMaxValue(100) - ), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - const region = interaction.options.getString("region") as Region; - const name = interaction.options.getString("username")!; - const limit = interaction.options.getInteger("limit") ?? 25; - const trimmedSearch = name.trim(); - const players = usernamePattern.test(trimmedSearch) - ? await fetchBlitz(region, "account/list", { - search: trimmedSearch, - limit, - }) - : []; - - return embedInfo( - literals(strings.bot.commands.search_players.body.title, { - search: markdownEscape(trimmedSearch), - region: strings.common.regions.normal[region], - }), - `\`\`\`${ - players.length === 0 - ? strings.bot.commands.search_players.body.no_results - : players.map((player) => player.nickname).join("\n") - }\`\`\`` - ); - }, - }); -}); diff --git a/packages/bot/src/commands/stats.tsx b/packages/bot/src/commands/stats.tsx deleted file mode 100644 index 38337cd09..000000000 --- a/packages/bot/src/commands/stats.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { emblemURL, getAccountInfo, getClanAccountInfo } from "@blitzkit/core"; -import { Locale } from "discord.js"; -import { AllStatsOverview } from "../components/AllStatsOverview"; -import { CommandWrapper } from "../components/CommandWrapper"; -import { NoData } from "../components/NoData"; -import { TitleBar } from "../components/TitleBar"; -import { filtersToDescription } from "../core/blitzkit/filtersToDescription"; -import { filterStats, type StatFilters } from "../core/blitzstars/filterStats"; -import { getStatsInPeriod } from "../core/blitzstars/getStatsInPeriod"; -import { addPeriodicFilterOptions } from "../core/discord/addPeriodicFilterOptions"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { autocompleteTanks } from "../core/discord/autocompleteTanks"; -import { autocompleteUsername } from "../core/discord/autocompleteUsername"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { getFiltersFromButton } from "../core/discord/getFiltersFromButton"; -import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton"; -import { type ResolvedPeriod } from "../core/discord/resolvePeriodFromCommand"; -import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton"; -import { type ResolvedPlayer } from "../core/discord/resolvePlayerFromCommand"; -import { translator } from "../core/localization/translator"; -import type { CommandRegistry } from "../events/interactionCreate"; - -async function render( - { region, id }: ResolvedPlayer, - { start, end, name }: ResolvedPeriod, - filters: StatFilters, - locale: Locale, -) { - const { nickname } = await getAccountInfo(region, id); - const clan = (await getClanAccountInfo(region, id, ["clan"]))?.clan; - const clanImage = clan ? emblemURL(clan.emblem_set_id) : undefined; - const diffedTankStats = await getStatsInPeriod( - region, - id, - start, - end, - locale, - ); - const { stats, supplementary } = await filterStats(diffedTankStats, filters); - const filterDescriptions = await filtersToDescription(filters, locale); - - return ( - - - - {!stats.battles && } - {stats.battles > 0 && ( - - )} - - ); -} - -export const statsCommand = new Promise(async (resolve) => { - resolve({ - command: await addPeriodicFilterOptions( - createLocalizedCommand("stats"), - (option) => option.addStringOption(addUsernameChoices), - ), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - return strings.bot.commands.dead; - - // const player = await resolvePlayerFromCommand(interaction); - // const period = resolvePeriodFromCommand(player.region, interaction); - // const filters = await getFiltersFromCommand(interaction); - // const path = commandToURL(interaction, { - // ...player, - // ...getCustomPeriodParams(interaction), - // ...filters, - // }); - - // return Promise.all([ - // render(player, period, filters, interaction.locale), - // buttonRefresh(interaction, path), - // getBlitzStarsLinkButton(player.region, player.id), - // ]); - }, - - autocomplete: (interaction) => { - autocompleteUsername(interaction); - autocompleteTanks(interaction); - }, - - async button(interaction) { - const player = await resolvePlayerFromButton(interaction); - const period = resolvePeriodFromButton(player.region, interaction); - const filters = getFiltersFromButton(interaction); - - return await render(player, period, filters, interaction.locale); - }, - }); -}); diff --git a/packages/bot/src/commands/today.ts b/packages/bot/src/commands/today.ts deleted file mode 100644 index 65256cad9..000000000 --- a/packages/bot/src/commands/today.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { gameDefinitions } from "../core/blitzkit/nonBlockingPromises"; -import { addFilterOptions } from "../core/discord/addFilterOptions"; -import { addUsernameChoices } from "../core/discord/addUsernameChoices"; -import { autocompleteTanks } from "../core/discord/autocompleteTanks"; -import { autocompleteUsername } from "../core/discord/autocompleteUsername"; -import { createLocalizedCommand } from "../core/discord/createLocalizedCommand"; -import { getFiltersFromButton } from "../core/discord/getFiltersFromButton"; -import { resolvePeriodFromButton } from "../core/discord/resolvePeriodFromButton"; -import { resolvePlayerFromButton } from "../core/discord/resolvePlayerFromButton"; -import { translator } from "../core/localization/translator"; -import { type CommandRegistry } from "../events/interactionCreate"; -import { renderBreakdown } from "./breakdown/render"; - -export const todayCommand = new Promise(async (resolve) => { - const awaitedGameDefinitions = await gameDefinitions; - - resolve({ - command: addFilterOptions( - createLocalizedCommand("today"), - awaitedGameDefinitions.nations, - ).addStringOption(addUsernameChoices), - - async handler(interaction) { - const { strings } = translator(interaction.locale); - return strings.bot.commands.dead; - - // const player = await resolvePlayerFromCommand(interaction); - // const period = resolvePeriodFromCommand( - // player.region, - // interaction, - // 'today', - // ); - // const filters = await getFiltersFromCommand(interaction); - // const path = commandToURL(interaction, { - // ...player, - // ...getCustomPeriodParams(interaction, true), - // ...filters, - // }); - - // return [ - // ...(await renderBreakdown(player, period, filters, interaction.locale)), - // buttonRefresh(interaction, path), - // await getBlitzStarsLinkButton(player.region, player.id), - // ]; - }, - - autocomplete: (interaction) => { - autocompleteUsername(interaction); - autocompleteTanks(interaction); - }, - - async button(interaction) { - const player = await resolvePlayerFromButton(interaction); - const period = resolvePeriodFromButton(player.region, interaction); - const filters = getFiltersFromButton(interaction); - - return await renderBreakdown(player, period, filters, interaction.locale); - }, - }); -}); diff --git a/packages/bot/src/components/AllStatsOverview/components/HeroStat/components/Glow.tsx b/packages/bot/src/components/AllStatsOverview/components/HeroStat/components/Glow.tsx deleted file mode 100644 index 1a3a00277..000000000 --- a/packages/bot/src/components/AllStatsOverview/components/HeroStat/components/Glow.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { type ComponentProps } from "react"; - -export interface GlowProps extends ComponentProps<"div"> { - color: string; - direction?: "default" | "reverse"; - rotation?: number; - startOpacity?: number; - endOpacity?: number; -} - -const width = 16; - -export function Glow({ - color, - direction = "default", - rotation = 0, - style, - startOpacity = 0, - endOpacity = 1, - ...props -}: GlowProps) { - const color1 = `${color}${Math.round(endOpacity * 255) - .toString(16) - .padStart(2, "0")}`; - const color2 = `${color}${Math.round(startOpacity * 255) - .toString(16) - .padStart(2, "0")}`; - const background = `linear-gradient(${ - direction === "default" ? 0 : 180 - }deg, ${color1} 0%, ${color2} 100%)`; - - return ( -
-
-
-
-
-
-
- ); -} diff --git a/packages/bot/src/components/AllStatsOverview/components/HeroStat/index.tsx b/packages/bot/src/components/AllStatsOverview/components/HeroStat/index.tsx deleted file mode 100644 index be3b5a0f2..000000000 --- a/packages/bot/src/components/AllStatsOverview/components/HeroStat/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { theme } from '../../../../stitches.config'; -import { Glow } from './components/Glow'; - -export interface WN8DisplayProps { - subtitle: string; - color: string; - value: string | number; -} - -export function HeroStat({ color, subtitle, value }: WN8DisplayProps) { - return ( -
- - -
-
- - {value} - - - {subtitle} - -
-
- - -
- ); -} diff --git a/packages/bot/src/components/AllStatsOverview/components/WN8SurroundingStat.tsx b/packages/bot/src/components/AllStatsOverview/components/WN8SurroundingStat.tsx deleted file mode 100644 index 62db93c41..000000000 --- a/packages/bot/src/components/AllStatsOverview/components/WN8SurroundingStat.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { theme } from '../../../stitches.config'; - -export enum WN8SurroundingStatAlign { - Left, - Right, -} - -export interface WN8SurroundingStatProps { - label: string; - value: string | number; - align?: WN8SurroundingStatAlign; - padded?: boolean; -} - -export function WN8SurroundingStat({ - label, - value, - align = WN8SurroundingStatAlign.Left, - padded = false, -}: WN8SurroundingStatProps) { - return ( -
- - {value} - - - {label} - -
- ); -} diff --git a/packages/bot/src/components/AllStatsOverview/index.tsx b/packages/bot/src/components/AllStatsOverview/index.tsx deleted file mode 100644 index 62073cb00..000000000 --- a/packages/bot/src/components/AllStatsOverview/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { - getWN8Percentile, - Percentile, - type SupplementaryStats, -} from '@blitzkit/core'; -import { Locale } from 'discord.js'; -import { translator } from '../../core/localization/translator'; -import { PERCENTILE_COLORS } from '../PercentileIndicator/constants'; -import { HeroStat } from './components/HeroStat'; -import { - WN8SurroundingStat, - WN8SurroundingStatAlign, -} from './components/WN8SurroundingStat'; - -export interface AllStatsOverviewProps { - stats: { - battles: number; - wins: number; - damage_dealt: number; - damage_received: number; - survived_battles: number; - hits: number; - shots: number; - frags: number; - }; - supplementaryStats: SupplementaryStats; - locale: Locale; -} - -export function AllStatsOverview({ - stats, - locale, - supplementaryStats, -}: AllStatsOverviewProps) { - const { strings } = translator(locale); - const percentile = - typeof supplementaryStats.WN8 === 'number' - ? getWN8Percentile(supplementaryStats.WN8) - : Percentile.VeryBad; - const color = PERCENTILE_COLORS[percentile]; - - const heroStat = - typeof supplementaryStats.WN8 === 'number' - ? supplementaryStats.WN8.toFixed(0) - : '--'; - - return ( -
-
- - - - -
- - - -
- - - - -
-
- ); -} diff --git a/packages/bot/src/components/Breakdown/components/Root.tsx b/packages/bot/src/components/Breakdown/components/Root.tsx deleted file mode 100644 index cbbe20573..000000000 --- a/packages/bot/src/components/Breakdown/components/Root.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { type ReactNode } from "react"; - -interface RootProps { - children: ReactNode; -} - -export function Root({ children }: RootProps) { - return ( -
- {children} -
- ); -} diff --git a/packages/bot/src/components/Breakdown/components/Row.tsx b/packages/bot/src/components/Breakdown/components/Row.tsx deleted file mode 100644 index a5af946d6..000000000 --- a/packages/bot/src/components/Breakdown/components/Row.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { - Percentile, - TankClass, - TankType, - TREE_TYPE_ICONS, -} from "@blitzkit/core"; -import { createColors } from "bepaint"; -import { - type AccentColor, - type GrayColor, - PALETTES, -} from "../../../core/radix/radixColors"; -import { RowStat } from "./RowStat"; - -export interface RowStatItem { - title: string; - current?: number | string; - career?: number | string; - delta?: number; - percentile?: Percentile; -} - -interface RowProps { - displayType?: "tank" | "summary"; - naked?: boolean; - title: string; - minimized?: boolean; - type?: TankType; - class?: TankClass; - color?: AccentColor | GrayColor; - stats: (RowStatItem | undefined)[]; -} - -export function Row({ - color = "slate", - stats, - title, - minimized, - class: tankClass, - type, - naked = false, - displayType, -}: RowProps) { - const palette = PALETTES[`${color}Dark`]; - const theme = createColors(palette); - - return ( -
-
- {displayType === "tank" && tankClass !== undefined && ( - {TankClass[tankClass]} - )} - - - {title} - -
- -
- {stats.map( - (row, index) => - row && ( - - ), - )} -
-
- ); -} diff --git a/packages/bot/src/components/Breakdown/components/RowStat.tsx b/packages/bot/src/components/Breakdown/components/RowStat.tsx deleted file mode 100644 index 430f8b6a6..000000000 --- a/packages/bot/src/components/Breakdown/components/RowStat.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { Percentile } from "@blitzkit/core"; -import { createColors } from "bepaint"; -import { - type AccentColor, - type GrayColor, - PALETTES, -} from "../../../core/radix/radixColors"; -import { PercentileIndicator } from "../../PercentileIndicator"; - -interface RowStatProps { - name: string; - value?: string | number; - delta?: number; - percentile?: Percentile; - minimized?: boolean; - color?: AccentColor | GrayColor; -} - -export function RowStat({ - name, - value, - delta, - percentile, - minimized = false, - color = "slate", -}: RowStatProps) { - const theme = createColors(PALETTES[`${color}Dark`]); - - return ( -
-
- {percentile === undefined && delta !== undefined && delta !== 0 && ( - {delta 0 ? "Increase" : "Decrease"} - src={ - (delta ?? 0) > 0 - ? "https://i.imgur.com/qbjiXa1.png" - : "https://i.imgur.com/3uyNhun.png" - } - style={{ width: 12, height: 12 }} - /> - )} - {percentile !== undefined && ( - - )} - - - {value} - -
- - {!minimized && ( - - {name} - - )} -
- ); -} diff --git a/packages/bot/src/components/Breakdown/index.ts b/packages/bot/src/components/Breakdown/index.ts deleted file mode 100644 index bf85c5f20..000000000 --- a/packages/bot/src/components/Breakdown/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './components/Root'; -export * from './components/Row'; -export * from './components/RowStat'; diff --git a/packages/bot/src/components/CommandWrapper.tsx b/packages/bot/src/components/CommandWrapper.tsx deleted file mode 100644 index c4dd6b066..000000000 --- a/packages/bot/src/components/CommandWrapper.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { type ReactNode } from "react"; -import { theme } from "../stitches.config"; - -interface CommandWrapperProps { - children: ReactNode; - fat?: boolean; -} - -export function CommandWrapper({ children, fat }: CommandWrapperProps) { - return ( -
- {children} -
- ); -} diff --git a/packages/bot/src/components/DeltaCaret.tsx b/packages/bot/src/components/DeltaCaret.tsx deleted file mode 100644 index 15c1ae8aa..000000000 --- a/packages/bot/src/components/DeltaCaret.tsx +++ /dev/null @@ -1,21 +0,0 @@ -interface DeltaCaretProps { - delta: number; -} - -export function DeltaCaret({ delta }: DeltaCaretProps) { - return ( - {delta 0 ? 'Increase' : 'Decrease'} - style={{ - width: 16, - height: 16, - objectFit: 'contain', - }} - src={ - delta > 0 - ? 'https://i.imgur.com/gjI4a5f.png' - : 'https://i.imgur.com/lLGZygN.png' - } - /> - ); -} diff --git a/packages/bot/src/components/Flag.tsx b/packages/bot/src/components/Flag.tsx deleted file mode 100644 index 110b71f95..000000000 --- a/packages/bot/src/components/Flag.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { asset } from '@blitzkit/core'; - -interface FlagProps { - nation: string; -} - -export function Flag({ nation }: FlagProps) { - return ( -
- {nation} -
- ); -} diff --git a/packages/bot/src/components/GenericAllStats/components/Card.tsx b/packages/bot/src/components/GenericAllStats/components/Card.tsx deleted file mode 100644 index 2f0922c29..000000000 --- a/packages/bot/src/components/GenericAllStats/components/Card.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Percentile } from '@blitzkit/core'; -import { theme } from '../../../stitches.config'; -import { PercentileIndicator } from '../../PercentileIndicator'; - -interface CardItem { - label: string; - value: string | number | undefined; - percentile?: Percentile; -} - -interface CardProps { - items: CardItem[]; - title: string; -} - -export function Card({ items, title }: CardProps) { - return ( -
- - {title} - - -
- {items.map((item) => ( -
- - {item.label} - - -
- -
- {item.percentile !== undefined && ( - - )} - - {item.value === undefined || Number.isNaN(item.value) - ? '--' - : item.value} - -
-
- ))} -
-
- ); -} diff --git a/packages/bot/src/components/GenericAllStats/components/Root.tsx b/packages/bot/src/components/GenericAllStats/components/Root.tsx deleted file mode 100644 index 5c362f44b..000000000 --- a/packages/bot/src/components/GenericAllStats/components/Root.tsx +++ /dev/null @@ -1,17 +0,0 @@ -interface RootProps { - children: React.ReactNode; -} - -export function Root({ children }: RootProps) { - return ( -
- {children} -
- ); -} diff --git a/packages/bot/src/components/GenericAllStats/components/Row.tsx b/packages/bot/src/components/GenericAllStats/components/Row.tsx deleted file mode 100644 index bd5cfa17e..000000000 --- a/packages/bot/src/components/GenericAllStats/components/Row.tsx +++ /dev/null @@ -1,7 +0,0 @@ -interface RowProps { - children: React.ReactNode; -} - -export function Row({ children }: RowProps) { - return
{children}
; -} diff --git a/packages/bot/src/components/GenericAllStats/index.tsx b/packages/bot/src/components/GenericAllStats/index.tsx deleted file mode 100644 index 36b687132..000000000 --- a/packages/bot/src/components/GenericAllStats/index.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { - type BlitzStats, - getWN8Percentile, - isNumber, - type SupplementaryStats, -} from '@blitzkit/core'; -import { Locale } from 'discord.js'; -import { translator } from '../../core/localization/translator'; -import { Card } from './components/Card'; -import { Root } from './components/Root'; -import { Row } from './components/Row'; - -interface GenericAllStatsProps { - stats: BlitzStats; - supplementaryStats?: SupplementaryStats; - locale: Locale; -} - -export function GenericAllStats({ - stats, - supplementaryStats, - locale, -}: GenericAllStatsProps) { - const { strings } = translator(locale); - const damageRatio = stats.damage_dealt / stats.damage_received; - const killsToDeathRatio = - stats.frags / (stats.battles - stats.survived_battles); - - return ( - - - - - - - - - - - ); -} diff --git a/packages/bot/src/components/GenericStats/components/GenericStatsColumn.tsx b/packages/bot/src/components/GenericStats/components/GenericStatsColumn.tsx deleted file mode 100644 index 5346cde64..000000000 --- a/packages/bot/src/components/GenericStats/components/GenericStatsColumn.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { type GenericStat } from ".."; -import { GenericStatsRow } from "./GenericStatsRow"; - -interface GenericStatsColumnProps { - stats: GenericStat[]; -} - -export function GenericStatsColumn({ stats }: GenericStatsColumnProps) { - return ( -
- {stats.map((stat) => ( - - ))} -
- ); -} diff --git a/packages/bot/src/components/GenericStats/components/GenericStatsRow.tsx b/packages/bot/src/components/GenericStats/components/GenericStatsRow.tsx deleted file mode 100644 index 1db1c6bda..000000000 --- a/packages/bot/src/components/GenericStats/components/GenericStatsRow.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { type GenericStat } from ".."; -import { theme } from "../../../stitches.config"; -import { PercentileIndicator } from "../../PercentileIndicator"; - -interface GenericStatsRowProps { - stat: GenericStat; -} - -export function GenericStatsRow({ stat }: GenericStatsRowProps) { - return ( -
- - {stat[0]} - - - - {stat[2] !== undefined && } - {stat[1]} - -
- ); -} diff --git a/packages/bot/src/components/GenericStats/index.tsx b/packages/bot/src/components/GenericStats/index.tsx deleted file mode 100644 index ae23b18e9..000000000 --- a/packages/bot/src/components/GenericStats/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Percentile } from '@blitzkit/core'; -import { GenericStatsColumn } from './components/GenericStatsColumn'; - -export type GenericStat = - | [string, string | number | undefined] - | [string, string | number | undefined, Percentile | undefined]; - -export interface GenericStatsProps { - stats: GenericStat[]; - columnCount?: number; -} - -export function GenericStats({ stats, columnCount = 2 }: GenericStatsProps) { - const filteredStats = stats.filter(([, value]) => value !== undefined); - const itemsPerRow = Math.ceil(filteredStats.length / columnCount); - const columns: GenericStat[][] = []; - - filteredStats.forEach((stat, index) => { - const column = Math.floor(index / itemsPerRow); - if (!columns[column]) columns[column] = []; - columns[column].push(stat); - }); - - return ( -
- {columns.map((stats) => ( - stat[0]).join('-')} - stats={stats} - /> - ))} -
- ); -} diff --git a/packages/bot/src/components/Graph/components/Line/constants.ts b/packages/bot/src/components/Graph/components/Line/constants.ts deleted file mode 100644 index 74170a3a8..000000000 --- a/packages/bot/src/components/Graph/components/Line/constants.ts +++ /dev/null @@ -1,11 +0,0 @@ -export enum LineColor { - Red, - Green, - Blue, - White, -} -export enum LineStyle { - Solid, - Dashes, - DashesAndDots, -} diff --git a/packages/bot/src/components/Graph/components/Line/index.tsx b/packages/bot/src/components/Graph/components/Line/index.tsx deleted file mode 100644 index ad0beda82..000000000 --- a/packages/bot/src/components/Graph/components/Line/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { blue, green, red, slateDark } from '@radix-ui/colors'; -import { GRAPH_HEIGHT, GRAPH_WIDTH } from '../Root/constants'; -import { LineColor, LineStyle } from './constants'; - -const LINE_COLORS = { - [LineColor.Red]: red.red9, - [LineColor.Green]: green.green9, - [LineColor.Blue]: blue.blue9, - [LineColor.White]: slateDark.slate11, -}; - -const LINE_STYLES = { - [LineStyle.Solid]: undefined, - [LineStyle.Dashes]: '8, 8', - [LineStyle.DashesAndDots]: '1, 4, 8, 4', -}; - -export type PlotItem = [number, number]; -type Plot = PlotItem[]; - -interface LineProps { - plot: Plot; - minY?: number; - maxY?: number; - color?: LineColor; - style?: LineStyle; - lineWidth?: number; -} - -export function Line({ - plot, - minY: providedMinY, - maxY: providedMaxY, - color = LineColor.Red, - style = LineStyle.Solid, - lineWidth = 2, -}: LineProps) { - const xs = plot.map(([x]) => x); - const minX = Math.min(...xs); - const maxX = Math.max(...xs); - const ys = plot.map(([, y]) => y); - const minY = providedMinY ?? Math.min(...ys); - const maxY = providedMaxY ?? Math.max(...ys); - const rangeX = maxX - minX; - const rangeY = maxY - minY; - const points = plot - .map( - ([x, y]) => - `${(x - minX) * (GRAPH_WIDTH / rangeX)},${ - GRAPH_HEIGHT - (y - minY) * (GRAPH_HEIGHT / rangeY) - }`, - ) - .join(' '); - - return ( - - ); -} diff --git a/packages/bot/src/components/Graph/components/Margin/constants.ts b/packages/bot/src/components/Graph/components/Margin/constants.ts deleted file mode 100644 index fab153d6c..000000000 --- a/packages/bot/src/components/Graph/components/Margin/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum MarginOrientation { - Vertical, - Horizontal, -} diff --git a/packages/bot/src/components/Graph/components/Margin/index.tsx b/packages/bot/src/components/Graph/components/Margin/index.tsx deleted file mode 100644 index eb15c732c..000000000 --- a/packages/bot/src/components/Graph/components/Margin/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { times } from 'lodash-es'; -import { theme } from '../../../../stitches.config'; -import { type MarginInputProps } from '../Root'; -import { MarginOrientation } from './constants'; - -interface MarginProps extends MarginInputProps { - verticalSeparations: number; - orientation?: MarginOrientation; -} - -export function Margin({ - verticalSeparations, - max, - min, - suffix, - precision = 0, - orientation = MarginOrientation.Vertical, -}: MarginProps) { - const isVertical = orientation === MarginOrientation.Vertical; - - return ( -
- {times(verticalSeparations, (index) => ( - - {`${( - (max - min) * - ((verticalSeparations - (index + 1)) / - (verticalSeparations - 1)) + - min - ).toFixed(precision)}${suffix ?? ''}`} - - ))} -
- ); -} diff --git a/packages/bot/src/components/Graph/components/Root/constants.ts b/packages/bot/src/components/Graph/components/Root/constants.ts deleted file mode 100644 index 0ff4e7d6b..000000000 --- a/packages/bot/src/components/Graph/components/Root/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const GRAPH_WIDTH = 384; -export const GRAPH_HEIGHT = 320; diff --git a/packages/bot/src/components/Graph/components/Root/index.tsx b/packages/bot/src/components/Graph/components/Root/index.tsx deleted file mode 100644 index 86ac910f7..000000000 --- a/packages/bot/src/components/Graph/components/Root/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import { times } from "lodash-es"; -import { type ReactNode } from "react"; -import { theme } from "../../../../stitches.config"; -import { Margin } from "../Margin"; -import { MarginOrientation } from "../Margin/constants"; -import { GRAPH_HEIGHT, GRAPH_WIDTH } from "./constants"; - -export interface MarginInputProps { - min: number; - max: number; - suffix?: string; - precision?: number; -} - -interface RootProps { - children: ReactNode; - verticalMargin?: MarginInputProps; - horizontalMargin?: MarginInputProps; - separations?: number; - xMinLabel: string; - xMaxLabel: string; - xTitle?: string; -} - -export function Root({ - children, - verticalMargin, - horizontalMargin, - separations = 5, - xMinLabel, - xMaxLabel, - xTitle, -}: RootProps) { - return ( -
-
- - {xMinLabel} - - - {xMaxLabel} - -
- -
- {verticalMargin && ( - - )} - - - {times(separations - 2, (index) => { - const y = GRAPH_HEIGHT * ((index + 1) / (separations - 1)); - - return ( - - ); - })} - - {children} - -
- - {horizontalMargin && ( - - )} - - {xTitle !== undefined && ( -
- - {xTitle} - -
- )} -
- ); -} diff --git a/packages/bot/src/components/Graph/index.ts b/packages/bot/src/components/Graph/index.ts deleted file mode 100644 index 5aaadb77e..000000000 --- a/packages/bot/src/components/Graph/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './components/Line'; -export * from './components/Margin'; -export * from './components/Root'; diff --git a/packages/bot/src/components/Leaderboard/components/Gap.tsx b/packages/bot/src/components/Leaderboard/components/Gap.tsx deleted file mode 100644 index f1fa3f16c..000000000 --- a/packages/bot/src/components/Leaderboard/components/Gap.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { theme } from '../../../stitches.config'; - -interface GapProps { - message: string; -} - -export function Gap({ message: number }: GapProps) { - return ( -
- - {number.toLocaleString()} - -
- ); -} diff --git a/packages/bot/src/components/Leaderboard/components/Item.tsx b/packages/bot/src/components/Leaderboard/components/Item.tsx deleted file mode 100644 index 249221e41..000000000 --- a/packages/bot/src/components/Leaderboard/components/Item.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { type ComponentProps } from "react"; -import { theme } from "../../../stitches.config"; - -interface ItemProps extends ComponentProps<"div"> { - position: number; - deltaPosition?: number; - score?: number; - deltaScore?: number; - nickname: string; - clan?: string; - highlight?: boolean; -} - -export function Item({ - position, - deltaPosition, - score, - deltaScore, - nickname, - clan, - highlight, - ...props -}: ItemProps) { - return ( -
- - {position.toLocaleString()}. - - -
- - {nickname} - - {clan && ( - - [{clan}] - - )} -
- - {deltaPosition !== undefined && deltaPosition !== 0 && ( -
- {deltaPosition 0 ? "Increase" : "Decrease"} - style={{ - width: 8, - height: 8, - }} - src={ - deltaPosition > 0 - ? "https://i.imgur.com/qbjiXa1.png" - : "https://i.imgur.com/3uyNhun.png" - } - /> - - {Math.abs(deltaPosition).toLocaleString()} - -
- )} - - {/* TODO: lol remove this spacer div for a better solution */} -
- -
- {deltaScore !== 0 && deltaScore !== undefined && ( - - {Math.abs(deltaScore).toLocaleString()} - - )} - {deltaScore !== 0 && deltaScore !== undefined && ( - {deltaScore 0 ? "Increase" : "Decrease"} - style={{ - width: 8, - height: 8, - }} - src={ - deltaScore > 0 - ? "https://i.imgur.com/qbjiXa1.png" - : "https://i.imgur.com/3uyNhun.png" - } - /> - )} - - {score !== undefined && ( - - {score.toLocaleString()} - - )} -
-
- ); -} diff --git a/packages/bot/src/components/Leaderboard/components/Root.tsx b/packages/bot/src/components/Leaderboard/components/Root.tsx deleted file mode 100644 index d812cf3c7..000000000 --- a/packages/bot/src/components/Leaderboard/components/Root.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { type ReactNode } from 'react'; - -interface RootProps { - children: ReactNode; -} - -export function Root({ children }: RootProps) { - return ( -
- {children} -
- ); -} diff --git a/packages/bot/src/components/Leaderboard/index.ts b/packages/bot/src/components/Leaderboard/index.ts deleted file mode 100644 index 4334194b1..000000000 --- a/packages/bot/src/components/Leaderboard/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './components/Gap'; -export * from './components/Item'; -export * from './components/Root'; diff --git a/packages/bot/src/components/NoData/constants.ts b/packages/bot/src/components/NoData/constants.ts deleted file mode 100644 index a01763279..000000000 --- a/packages/bot/src/components/NoData/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum NoDataType { - BattlesInPeriod, - PlayersInPeriod, - TanksFound, -} diff --git a/packages/bot/src/components/NoData/index.tsx b/packages/bot/src/components/NoData/index.tsx deleted file mode 100644 index 7fa7227aa..000000000 --- a/packages/bot/src/components/NoData/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Locale } from 'discord.js'; -import { translator } from '../../core/localization/translator'; -import { theme } from '../../stitches.config'; - -interface NoDataProps { - type: - | 'battles_in_period' - | 'players_in_period' - | 'tanks_found' - | 'battles_in_season'; - locale: Locale; -} - -export function NoData({ type, locale }: NoDataProps) { - const { strings } = translator(locale); - - return ( -
- - {strings.bot.common.no_data[type]} - -
- ); -} diff --git a/packages/bot/src/components/PercentileIndicator/constants.ts b/packages/bot/src/components/PercentileIndicator/constants.ts deleted file mode 100644 index b5cefb554..000000000 --- a/packages/bot/src/components/PercentileIndicator/constants.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Percentile } from '@blitzkit/core'; -import { - amber, - blue, - green, - lime, - orange, - pink, - purple, - red, - slate, - teal, -} from '@radix-ui/colors'; - -export const PERCENTILE_COLORS = { - [Percentile.VeryBad]: slate.slate9, - [Percentile.Bad]: red.red9, - [Percentile.BelowAverage]: orange.orange9, - [Percentile.Average]: amber.amber10, - [Percentile.AboveAverage]: lime.lime8, - [Percentile.Good]: green.green9, - [Percentile.VeryGood]: teal.teal9, - [Percentile.Great]: blue.blue9, - [Percentile.Unicum]: pink.pink9, - [Percentile.SuperUnicum]: purple.purple9, -}; diff --git a/packages/bot/src/components/PercentileIndicator/index.tsx b/packages/bot/src/components/PercentileIndicator/index.tsx deleted file mode 100644 index f7c0f5b68..000000000 --- a/packages/bot/src/components/PercentileIndicator/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Percentile } from '@blitzkit/core'; -import { PERCENTILE_COLORS } from './constants'; - -interface PercentileIndicatorProps { - percentile: Percentile; -} - -export function PercentileIndicator({ percentile }: PercentileIndicatorProps) { - return ( -
- ); -} diff --git a/packages/bot/src/components/Tanks/components/Column.tsx b/packages/bot/src/components/Tanks/components/Column.tsx deleted file mode 100644 index 6e3549c89..000000000 --- a/packages/bot/src/components/Tanks/components/Column.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { type ReactNode } from 'react'; - -interface ColumnProps { - children: ReactNode; -} - -export function Column({ children }: ColumnProps) { - return ( -
- {children} -
- ); -} diff --git a/packages/bot/src/components/Tanks/components/Item/index.tsx b/packages/bot/src/components/Tanks/components/Item/index.tsx deleted file mode 100644 index f33342c3a..000000000 --- a/packages/bot/src/components/Tanks/components/Item/index.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { TankClass, TankType, TREE_TYPE_ICONS } from "@blitzkit/core"; -import { theme } from "../../../../stitches.config"; - -export interface ItemProps { - tankClass?: TankClass; - name: string; - tankType: TankType; -} - -const TREE_TYPE_COLOR = { - [TankType.TANK_TYPE_RESEARCHABLE]: "", - [TankType.TANK_TYPE_PREMIUM]: "_amber", - [TankType.TANK_TYPE_COLLECTOR]: "_blue", -} as const; - -export function Item({ - tankClass: tankType, - name, - tankType: treeType, -}: ItemProps) { - return ( -
-
- {tankType && ( - {TankType[tankType]} - )} - - {name} - -
- - {/* {image && ( - // TODO: remove hardcoded dimensions when satori fixes non-width unloadable images - {name} - )} */} -
- ); -} diff --git a/packages/bot/src/components/Tanks/components/Root.tsx b/packages/bot/src/components/Tanks/components/Root.tsx deleted file mode 100644 index a5d2e3107..000000000 --- a/packages/bot/src/components/Tanks/components/Root.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { type ReactNode } from "react"; - -interface RootProps { - children: ReactNode; -} - -export function Root({ children }: RootProps) { - return ( -
- {children} -
- ); -} diff --git a/packages/bot/src/components/Tanks/components/Row.tsx b/packages/bot/src/components/Tanks/components/Row.tsx deleted file mode 100644 index f5520127d..000000000 --- a/packages/bot/src/components/Tanks/components/Row.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { type ReactNode } from "react"; - -interface RowProps { - children: ReactNode; -} - -export function Row({ children }: RowProps) { - return ( -
- {children} -
- ); -} diff --git a/packages/bot/src/components/Tanks/components/Title.tsx b/packages/bot/src/components/Tanks/components/Title.tsx deleted file mode 100644 index 10e7d6281..000000000 --- a/packages/bot/src/components/Tanks/components/Title.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { theme } from '../../../stitches.config'; - -interface TitleProps { - children: string; -} - -export function Title({ children }: TitleProps) { - return ( - - {children} - - ); -} diff --git a/packages/bot/src/components/Tanks/index.ts b/packages/bot/src/components/Tanks/index.ts deleted file mode 100644 index 7042919a3..000000000 --- a/packages/bot/src/components/Tanks/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './components/Column'; -export * from './components/Item'; -export * from './components/Root'; -export * from './components/Row'; -export * from './components/Title'; diff --git a/packages/bot/src/components/TierWeights.tsx b/packages/bot/src/components/TierWeights.tsx deleted file mode 100644 index 415c6b6bc..000000000 --- a/packages/bot/src/components/TierWeights.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import type { TierWeightsRecord } from "@blitzkit/core"; -import { theme } from "../stitches.config"; - -interface TierWeightsProps { - weights: TierWeightsRecord; -} - -export function TierWeights({ weights }: TierWeightsProps) { - return ( -
- {Object.entries(weights).map(([tier, weight]) => ( -
- {tier} -
- ))} -
- ); -} diff --git a/packages/bot/src/components/TitleBar.tsx b/packages/bot/src/components/TitleBar.tsx deleted file mode 100644 index e64d0db7b..000000000 --- a/packages/bot/src/components/TitleBar.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { theme } from '../stitches.config'; - -interface TitleBarProps { - title: string; - image?: string; - description?: string; -} - -export function TitleBar({ title, image, description }: TitleBarProps) { - return ( -
-
- {image && ( - {title} - )} - - - {title} - -
- - - {description} - -
- ); -} diff --git a/packages/bot/src/core/blitz/normalizeLeagueIcon.ts b/packages/bot/src/core/blitz/normalizeLeagueIcon.ts deleted file mode 100644 index 4c450ed19..000000000 --- a/packages/bot/src/core/blitz/normalizeLeagueIcon.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function normalizeLeagueIcon(url: string) { - return url.startsWith('http') ? url : `https:${url}`; -} diff --git a/packages/bot/src/core/blitz/resolveTankId.ts b/packages/bot/src/core/blitz/resolveTankId.ts deleted file mode 100644 index 4415a8362..000000000 --- a/packages/bot/src/core/blitz/resolveTankId.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { SEARCH_KEYS } from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { Locale } from "discord.js"; -import { go } from "fuzzysort"; -import markdownEscape from "markdown-escape"; -import { tankDefinitions, tankNames } from "../blitzkit/nonBlockingPromises"; -import { UserError } from "../blitzkit/userError"; -import { tankNamesTechTreeOnly } from "../discord/autocompleteTanks"; -import { translator } from "../localization/translator"; - -export async function resolveTankId( - tank: string | number, - locale: Locale, - techTreeOnly = false -) { - const { strings } = translator(locale); - const number = typeof tank === "string" ? Number(tank) : tank; - - if (Number.isNaN(number)) { - const searchResult = go( - `${tank}`, - await (techTreeOnly ? tankNamesTechTreeOnly : tankNames), - { keys: SEARCH_KEYS, limit: 1 } - ); - - if (searchResult.length === 0) { - throw new UserError( - literals(strings.bot.common.errors.tank_name_not_found, { - search: markdownEscape(`${tank}`), - }) - ); - } else { - return searchResult[0].obj.id; - } - } else { - const awaitedTankDefinitions = await tankDefinitions; - - if (awaitedTankDefinitions.tanks[number]) { - return number; - } else { - throw new UserError( - literals(strings.bot.common.errors.tank_id_not_found, { - search: number, - }) - ); - } - } -} diff --git a/packages/bot/src/core/blitz/searchClansAcrossRegionsBotWrapper.ts b/packages/bot/src/core/blitz/searchClansAcrossRegionsBotWrapper.ts deleted file mode 100644 index c06e03b30..000000000 --- a/packages/bot/src/core/blitz/searchClansAcrossRegionsBotWrapper.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { searchClansAcrossRegions } from '@blitzkit/core'; -import { Locale } from 'discord.js'; -import { UserError } from '../blitzkit/userError'; -import { translator } from '../localization/translator'; - -export async function searchClansAcrossRegionsBotWrapper( - search: string, - locale: Locale, - limit?: number, -) { - const { strings } = translator(locale); - const trimmed = search.trim(); - - if (trimmed.length < 3) { - throw new UserError(strings.bot.common.errors.query_too_small); - } - - if (trimmed.length > 100) { - throw new UserError(strings.bot.common.errors.query_too_long); - } - - return await searchClansAcrossRegions(trimmed, limit); -} diff --git a/packages/bot/src/core/blitz/searchPlayersAcrossRegionsBotWrapper.ts b/packages/bot/src/core/blitz/searchPlayersAcrossRegionsBotWrapper.ts deleted file mode 100644 index 89e473a8d..000000000 --- a/packages/bot/src/core/blitz/searchPlayersAcrossRegionsBotWrapper.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { searchPlayersAcrossRegions } from '@blitzkit/core'; -import { Locale } from 'discord.js'; -import { UserError } from '../blitzkit/userError'; -import { translator } from '../localization/translator'; - -export async function searchPlayersAcrossRegionsBotWrapper( - search: string, - locale: Locale, - limit?: number, -) { - const { strings } = translator(locale); - const trimmed = search.trim(); - - if (trimmed.length < 3) { - throw new UserError(strings.bot.common.errors.query_too_small); - } - - if (trimmed.length > 100) { - throw new UserError(strings.bot.common.errors.query_too_long); - } - - return await searchPlayersAcrossRegions(trimmed, limit); -} diff --git a/packages/bot/src/core/blitzkit/buildTechTreeLine.ts b/packages/bot/src/core/blitzkit/buildTechTreeLine.ts deleted file mode 100644 index e8e1ed463..000000000 --- a/packages/bot/src/core/blitzkit/buildTechTreeLine.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { fetchTankDefinitions } from '@blitzkit/core'; -import { resolveAncestry } from './resolveAncestry'; - -export async function buildTechTreeLine(start: number, end: number) { - const tankDefinitions = await fetchTankDefinitions(); - const endTank = tankDefinitions.tanks[end]; - if (!endTank.ancestors) throw new Error('End tank has no ancestors'); - const line = [end]; - - if (start === end) return line; - for (const ancestorId of endTank.ancestors) { - if ((await resolveAncestry(ancestorId)).includes(start)) { - const ancestorTree = await buildTechTreeLine(start, ancestorId); - line.push(...ancestorTree); - break; - } - } - - return line; -} diff --git a/packages/bot/src/core/blitzkit/filtersToDescription.ts b/packages/bot/src/core/blitzkit/filtersToDescription.ts deleted file mode 100644 index a8c8377cd..000000000 --- a/packages/bot/src/core/blitzkit/filtersToDescription.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { fetchTankDefinitions, TIER_ROMAN_NUMERALS } from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { Locale } from "discord.js"; -import { type StatFilters } from "../blitzstars/filterStats"; -import { translator } from "../localization/translator"; - -export async function filtersToDescription( - { nation, tier, type, class: tankClass, tank }: StatFilters, - locale: Locale, -) { - const { strings, bkLocale } = translator(locale); - const awaitedTankDefinitions = await fetchTankDefinitions(); - const info: string[] = []; - - // TODO: somehow localize tank names? - if (tank) return awaitedTankDefinitions.tanks[tank].name!.locales[bkLocale]; - if (nation) - info.push((strings.common.nations as Record)[nation]); - if (tier) { - info.push( - literals(strings.bot.common.filters.tier, { - tier: TIER_ROMAN_NUMERALS[tier], - }), - ); - } - if (tankClass) info.push(strings.common.tank_class_short[tankClass]); - if (type) info.push(strings.common.tree_type[type]); - - return info.length === 0 ? strings.bot.common.filters.none : info.join(", "); -} diff --git a/packages/bot/src/core/blitzkit/getBlitzFromDiscord.ts b/packages/bot/src/core/blitzkit/getBlitzFromDiscord.ts deleted file mode 100644 index 16088c64b..000000000 --- a/packages/bot/src/core/blitzkit/getBlitzFromDiscord.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { idToRegion } from '@blitzkit/core'; -import { flagUserActivity } from '../db/flagUserActivity'; -import { usersDatabase } from '../db/users'; - -export async function getBlitzFromDiscord(discordId: bigint) { - const unique = await usersDatabase.discordUser.findUnique({ - where: { discord_id: discordId }, - }); - - if (unique === null) return null; - - const id = Number(unique.blitz_id); - - await flagUserActivity(id); - return { id, region: idToRegion(id) }; -} diff --git a/packages/bot/src/core/blitzkit/getPeriodNow.ts b/packages/bot/src/core/blitzkit/getPeriodNow.ts deleted file mode 100644 index 030f2aa94..000000000 --- a/packages/bot/src/core/blitzkit/getPeriodNow.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getPeriodNow() { - return Date.now() / 1000; -} diff --git a/packages/bot/src/core/blitzkit/getPeriodStart.ts b/packages/bot/src/core/blitzkit/getPeriodStart.ts deleted file mode 100644 index aef8b4328..000000000 --- a/packages/bot/src/core/blitzkit/getPeriodStart.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getTimeDaysAgo, type Region } from '@blitzkit/core'; -import type { PeriodSize } from '../discord/addPeriodSubCommands'; - -export function getPeriodStart(region: Region, period: PeriodSize) { - if (period === 'career') return 0; - if (period === 'today') return getTimeDaysAgo(region, 1); - return getTimeDaysAgo(region, parseInt(period)); -} diff --git a/packages/bot/src/core/blitzkit/iconPng.ts b/packages/bot/src/core/blitzkit/iconPng.ts deleted file mode 100644 index c575b07b7..000000000 --- a/packages/bot/src/core/blitzkit/iconPng.ts +++ /dev/null @@ -1,16 +0,0 @@ -import sharp from "sharp"; - -const cache: Record = {}; - -export async function iconPng(url: string) { - if (!url.endsWith(".webp")) return url; - - if (!cache[url]) { - const imageResponse = await fetch(url); - const imageBufferWEBP = await imageResponse.arrayBuffer(); - const imageBufferPNG = await sharp(imageBufferWEBP).png().toBuffer(); - cache[url] = imageBufferPNG; - } - - return `data:image/png;base64,${cache[url].toString("base64")}`; -} diff --git a/packages/bot/src/core/blitzkit/jsxToPng.ts b/packages/bot/src/core/blitzkit/jsxToPng.ts deleted file mode 100644 index bc8ae24d0..000000000 --- a/packages/bot/src/core/blitzkit/jsxToPng.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { jsxToSvg } from "./jsxToSvg"; -import { svgToPng } from "./svgToPng"; - -export async function jsxToPng(jsx: React.JSX.Element) { - const svg = await jsxToSvg(jsx); - const png = svgToPng(svg); - - return png; -} diff --git a/packages/bot/src/core/blitzkit/jsxToPngThreaded.ts b/packages/bot/src/core/blitzkit/jsxToPngThreaded.ts deleted file mode 100644 index 16ee14401..000000000 --- a/packages/bot/src/core/blitzkit/jsxToPngThreaded.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { jsxToSvg } from "./jsxToSvg"; -import { RenderConfiguration } from "./renderConfiguration"; -import { svgToPngThreaded } from "./svgToPngThreaded"; - -export async function jsxToPngThreaded( - jsx: React.JSX.Element, - renderConfiguration: RenderConfiguration -) { - const svg = await jsxToSvg(jsx, renderConfiguration); - const png = await svgToPngThreaded(svg); - return png; -} diff --git a/packages/bot/src/core/blitzkit/jsxToSvg/Roboto-Black.ttf b/packages/bot/src/core/blitzkit/jsxToSvg/Roboto-Black.ttf deleted file mode 100644 index 96ddf0003d64c4c3c2d5d3ff119de17c548c0410..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 129228 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 S;t$MGZ{&B8(NcpQ=YIi$MEq<3 diff --git a/packages/bot/src/core/blitzkit/jsxToSvg/Roboto-Bold.ttf b/packages/bot/src/core/blitzkit/jsxToSvg/Roboto-Bold.ttf deleted file mode 100644 index 7a876e28e8e43ac9ab91fe361228c5e4c4e5253e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128676 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 TOP&AVyW^ewt}wmSK*#xSw!BZl diff --git a/packages/bot/src/core/blitzkit/jsxToSvg/index.ts b/packages/bot/src/core/blitzkit/jsxToSvg/index.ts deleted file mode 100644 index 40c0e7bb0..000000000 --- a/packages/bot/src/core/blitzkit/jsxToSvg/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { readFile } from "fs/promises"; -import satori from "satori"; -import { RenderConfiguration } from "../renderConfiguration"; -import robotoBlack from "./Roboto-Black.ttf"; -import robotoBold from "./Roboto-Bold.ttf"; -import roboto from "./Roboto.ttf"; - -const fontsPromise = [ - { file: roboto, weight: 400 as const }, - { file: robotoBold, weight: 700 as const }, - { file: robotoBlack, weight: 900 as const }, -].map(async ({ file, weight }) => { - const data = await readFile(`${__dirname}/${file}`); - return { name: "Roboto", weight, data }; -}); - -export async function jsxToSvg( - jsx: React.JSX.Element, - renderConfiguration = new RenderConfiguration() -) { - const fonts = await Promise.all(fontsPromise); - return await satori(jsx, { width: renderConfiguration.width, fonts }); -} diff --git a/packages/bot/src/core/blitzkit/nonBlockingPromises.ts b/packages/bot/src/core/blitzkit/nonBlockingPromises.ts deleted file mode 100644 index 647a955d0..000000000 --- a/packages/bot/src/core/blitzkit/nonBlockingPromises.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - fetchAverageDefinitions, - fetchDiscoveredIdsDefinitions, - fetchGameDefinitions, - fetchMapDefinitions, - fetchTankDefinitions, - fetchTankNames, -} from "@blitzkit/core"; - -export const averageDefinitions = fetchAverageDefinitions(); -export const tankDefinitions = fetchTankDefinitions(); -export const gameDefinitions = fetchGameDefinitions(); -export const tankNames = fetchTankNames(); -export const mapDefinitions = fetchMapDefinitions(); -export const discoveredIdsDefinitions = fetchDiscoveredIdsDefinitions(); diff --git a/packages/bot/src/core/blitzkit/processes.ts b/packages/bot/src/core/blitzkit/processes.ts deleted file mode 100644 index a639e25cc..000000000 --- a/packages/bot/src/core/blitzkit/processes.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { usersDatabase } from "../db/users"; -import { client } from "../discord/client"; - -const EXIT_EVENTS = ["SIGINT", "SIGTERM", "SIGUSR1", "SIGUSR2"]; - -let cleaningUp = false; - -async function cleanup(signal: string) { - if (cleaningUp) return; - cleaningUp = true; - - console.log(`Cleaning up... (${signal})`); - - try { - await Promise.all([usersDatabase.$disconnect(), client.destroy()]); - } catch (err) { - console.error("Cleanup error:", err); - } - - console.log("Gracefully exiting..."); - - process.exit(0); -} - -export function registerProcesses() { - process.on("uncaughtException", (err) => { - console.error("uncaughtException:", err); - }); - - process.on("unhandledRejection", (err) => { - console.error("unhandledRejection:", err); - }); - - client.on("error", console.error); - - EXIT_EVENTS.forEach((event) => process.on(event, () => cleanup(event))); -} diff --git a/packages/bot/src/core/blitzkit/renderConfiguration.ts b/packages/bot/src/core/blitzkit/renderConfiguration.ts deleted file mode 100644 index 508e20591..000000000 --- a/packages/bot/src/core/blitzkit/renderConfiguration.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface RenderConfiguration { - width: number; -} - -export class RenderConfiguration implements RenderConfiguration { - width = 480; - - constructor(configuration?: Partial) { - Object.assign(this, configuration); - } -} diff --git a/packages/bot/src/core/blitzkit/resolveAncestry.ts b/packages/bot/src/core/blitzkit/resolveAncestry.ts deleted file mode 100644 index e5ac8b5db..000000000 --- a/packages/bot/src/core/blitzkit/resolveAncestry.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { fetchTankDefinitions } from '@blitzkit/core'; - -export async function resolveAncestry(id: number) { - const tankDefinitions = await fetchTankDefinitions(); - const tank = tankDefinitions.tanks[id]; - const ancestors: number[] = []; - - if (tank.ancestors) { - for (const ancestor of tank.ancestors) { - if (!ancestors.includes(ancestor)) { - ancestors.push(ancestor); - - for (const ancestorLayer2 of await resolveAncestry(ancestor)) { - if (!ancestors.includes(ancestorLayer2)) { - ancestors.push(ancestorLayer2); - } - } - } - } - } - - return ancestors.sort( - (a, b) => tankDefinitions.tanks[b].tier - tankDefinitions.tanks[a].tier, - ); -} diff --git a/packages/bot/src/core/blitzkit/svgToPng.ts b/packages/bot/src/core/blitzkit/svgToPng.ts deleted file mode 100644 index 06ace7446..000000000 --- a/packages/bot/src/core/blitzkit/svgToPng.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Resvg } from '@resvg/resvg-js'; - -export function svgToPng(svg: string) { - return new Resvg(svg).render().asPng(); -} diff --git a/packages/bot/src/core/blitzkit/svgToPngThreaded.ts b/packages/bot/src/core/blitzkit/svgToPngThreaded.ts deleted file mode 100644 index 344fb4627..000000000 --- a/packages/bot/src/core/blitzkit/svgToPngThreaded.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Worker } from 'worker_threads'; - -const RENDER_WORKERS = 1; - -const workers = [...Array(RENDER_WORKERS)].map(() => { - const worker = new Worker('dist/bot/workers/render.js'); - console.log(`🟡 Launching render worker ${worker.threadId}`); - return worker; -}); -const queue: { - svg: string; - resolve: (png: Buffer) => void; -}[] = []; - -function manageQueue() { - if (queue.length > 0 && workers.length > 0) { - const { svg, resolve } = queue.shift()!; - const worker = workers.shift()!; - - function cleanup() { - workers.push(worker); - worker.off('message', handleMessage); - worker.off('error', handleError); - manageQueue(); - } - function handleMessage(png: Uint8Array) { - cleanup(); - resolve(Buffer.from(png)); - } - function handleError(error: Error) { - cleanup(); - throw error; - } - - worker.postMessage(svg); - worker.on('message', handleMessage); - worker.on('error', handleError); - } -} - -export function svgToPngThreaded(svg: string) { - return new Promise((resolve) => { - queue.push({ svg, resolve }); - manageQueue(); - }); -} diff --git a/packages/bot/src/core/blitzkit/tankIconPng.ts b/packages/bot/src/core/blitzkit/tankIconPng.ts deleted file mode 100644 index a15b09c8b..000000000 --- a/packages/bot/src/core/blitzkit/tankIconPng.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { tankIcon } from '@blitzkit/core'; -import { iconPng } from './iconPng'; - -export function tankIconPng(id: number) { - return iconPng(tankIcon(id)); -} diff --git a/packages/bot/src/core/blitzkit/userError.ts b/packages/bot/src/core/blitzkit/userError.ts deleted file mode 100644 index 44a647f15..000000000 --- a/packages/bot/src/core/blitzkit/userError.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Discriminator for user caused errors to not report in logs - */ -export class UserError { - constructor(public message: string) {} -} diff --git a/packages/bot/src/core/blitzstars/filterStats.ts b/packages/bot/src/core/blitzstars/filterStats.ts deleted file mode 100644 index 2936170a9..000000000 --- a/packages/bot/src/core/blitzstars/filterStats.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { TankClass, TankType } from "@blitzkit/core"; -import type { SupplementaryStats } from "@blitzkit/core/src/blitz/getAccountInfo"; -import { calculateWN8 } from "@blitzkit/core/src/statistics/calculateWN8"; -import type { BlitzStats } from "@blitzkit/core/src/statistics/compositeStats"; -import { sumAllStats } from "@blitzkit/core/src/statistics/sumAllStats"; -import { - averageDefinitions, - tankDefinitions, -} from "../blitzkit/nonBlockingPromises"; - -export interface StatFilters { - nation?: string; - tier?: number; - class?: TankClass; - type?: TankType; - tank?: number; -} - -export async function filterStats( - { diff, order }: DiffedTankStats, - filters: StatFilters, -) { - // tank, if provided, takes priority over all other filters - if (filters.tank) filters = { tank: filters.tank }; - - const awaitedTankDefinitions = await tankDefinitions; - const awaitedTankAverages = await averageDefinitions; - const filteredOrder = order.filter((id) => { - const entry = awaitedTankDefinitions.tanks[id]; - - return ( - entry && - (filters.nation === undefined || entry.nation === filters.nation) && - (filters.tier === undefined || entry.tier === filters.tier) && - (filters.class === undefined || entry.class === filters.class) && - (filters.type === undefined || - (filters.type === TankType.TANK_TYPE_COLLECTOR && - entry.type === TankType.TANK_TYPE_COLLECTOR) || - (filters.type === TankType.TANK_TYPE_PREMIUM && - entry.type === TankType.TANK_TYPE_PREMIUM) || - (filters.type === TankType.TANK_TYPE_RESEARCHABLE && - entry.type === TankType.TANK_TYPE_RESEARCHABLE)) && - (filters.tank === undefined || entry.id === filters.tank) - ); - }); - const stats = sumAllStats(filteredOrder.map((id) => diff[id])); - const battlesOfTanksWithAverages = filteredOrder.reduce( - (accumulator, id) => - awaitedTankAverages.averages[id] - ? accumulator + diff[id].battles - : accumulator, - 0, - ); - const battlesOfTanksWithTankDefinition = filteredOrder.reduce( - (accumulator, id) => - awaitedTankDefinitions.tanks[id] - ? accumulator + diff[id].battles - : accumulator, - 0, - ); - const supplementary = { - WN8: - filteredOrder.reduce( - (accumulator, id) => - awaitedTankAverages.averages[id] - ? accumulator + - calculateWN8(awaitedTankAverages.averages[id].mu, diff[id]) * - diff[id].battles - : accumulator, - 0, - ) / battlesOfTanksWithAverages, - tier: - filteredOrder.reduce( - (accumulator, id) => - awaitedTankDefinitions.tanks[id] - ? accumulator + - awaitedTankDefinitions.tanks[id]!.tier * diff[id].battles - : accumulator, - 0, - ) / battlesOfTanksWithTankDefinition, - } satisfies SupplementaryStats; - - return { stats, supplementary, filteredOrder }; -} - -export interface DiffedTankStats { - diff: Record; - order: number[]; -} diff --git a/packages/bot/src/core/blitzstars/getBlitzStarsLinkButton.ts b/packages/bot/src/core/blitzstars/getBlitzStarsLinkButton.ts deleted file mode 100644 index 9a17855e7..000000000 --- a/packages/bot/src/core/blitzstars/getBlitzStarsLinkButton.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getAccountInfo, type Region } from "@blitzkit/core"; -import { buttonLink } from "../discord/buttonLink"; - -export async function getBlitzStarsLinkButton( - region: Region, - id: number, - tankId?: number -) { - const { nickname } = await getAccountInfo(region, id); - - return buttonLink( - `https://www.blitzstars.com/player/${region}/${nickname}${ - tankId ? `/tank/${tankId!}` : "" - }`, - "BlitzStars" - ); -} diff --git a/packages/bot/src/core/blitzstars/getPlayerHistories.ts b/packages/bot/src/core/blitzstars/getPlayerHistories.ts deleted file mode 100644 index 0434b8116..000000000 --- a/packages/bot/src/core/blitzstars/getPlayerHistories.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { type Region, emptyAllStats, getAccountInfo } from '@blitzkit/core'; -import { - type GetHistoriesOptions, - type Histories, - type History, - getHistoriesDefaultOptions, -} from '../../types/histories'; -import type { PlayerStats } from './getPlayerStats'; - -export async function getPlayerHistories( - server: Region, - id: number, - options?: Partial, -) { - const mergedOptions = { ...getHistoriesDefaultOptions, ...options }; - const response = await fetch( - `https://www.blitzstars.com/api/playerstats/${id}`, - ); - const playerHistories = ((await response.json()) as PlayerStats).map( - (history) => - ({ - all: history.statistics.all, - last_battle_time: history.last_battle_time, - }) satisfies History, - ); - let lastNonCompliantIndex = -1; - let hasCompliedOnce = false; - const filtered = playerHistories.filter(({ last_battle_time }, index) => { - const complies = - last_battle_time >= mergedOptions.start && - last_battle_time <= mergedOptions.end; - - if (complies && !hasCompliedOnce) hasCompliedOnce = true; - if (!hasCompliedOnce) lastNonCompliantIndex = index; - - return complies; - }); - - const latestNodes: Histories = []; - - if (mergedOptions.includeLatestHistories) { - const { last_battle_time, statistics } = await getAccountInfo(server, id); - - latestNodes.push({ all: statistics.all, last_battle_time }); - } - - const previousNodes: Histories = []; - - if (mergedOptions.includePreviousHistories) { - if (lastNonCompliantIndex !== -1) { - previousNodes.push( - playerHistories[lastNonCompliantIndex] ?? - ({ - all: emptyAllStats, - last_battle_time: -Infinity, - } satisfies History), - ); - } - } - - return [...latestNodes, ...filtered, ...previousNodes] satisfies Histories; -} diff --git a/packages/bot/src/core/blitzstars/getPlayerStats.ts b/packages/bot/src/core/blitzstars/getPlayerStats.ts deleted file mode 100644 index 1555bc147..000000000 --- a/packages/bot/src/core/blitzstars/getPlayerStats.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { BlitzStats, Region } from "@blitzkit/core"; - -interface IndividualPlayerStats { - clan: { clan_id: number; name: string; tag: string }; - statistics: { - all: BlitzStats; - wn8: number; - wn7: number; - average_tier: number; - }; - last_battle_time: number; - nickname: string; - account_id: number; - achievements: { - account_id: number; - [key: string]: number; - }; - region: Region; -} - -export type PlayerStats = IndividualPlayerStats[]; - -export async function getPlayerStats< - Ids extends number | number[], - ReturnType = Ids extends number ? PlayerStats : PlayerStats[], ->(region: Region, ids: Ids) { - const stats = await Promise.all( - ((typeof ids === "number" ? [ids] : ids) as number[]).map((id) => - fetch(`https://www.blitzstars.com/api/playerstats/${id}`).then( - (response) => response.json() as Promise - ) - ) - ); - - if (typeof ids === "number") return stats[0] as ReturnType; - return stats as ReturnType; -} diff --git a/packages/bot/src/core/blitzstars/getStatsInPeriod.ts b/packages/bot/src/core/blitzstars/getStatsInPeriod.ts deleted file mode 100644 index 861f5c8a5..000000000 --- a/packages/bot/src/core/blitzstars/getStatsInPeriod.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { type BlitzStats, emptyAllStats, type Region } from "@blitzkit/core"; -import { Locale } from "discord.js"; -import type { DiffedTankStats } from "./filterStats"; -import { - getTankHistories, - type TankHistories, - type TankHistoryRaw, -} from "./getTankHistories"; - -const emptyTankHistoryNode: TankHistoryRaw = { - account_id: 0, - battle_life_time: 0, - last_battle_time: 0, - mark_of_mastery: 0, - tank_id: 0, - all: emptyAllStats, -}; - -// BIG TODO: merge this neatly with diffNormalizedTankStats -export async function getStatsInPeriod( - server: Region, - id: number, - start: number, - end: number, - locale: Locale -) { - const history = await getTankHistories(server, id, locale, { - includeLatestHistories: true, - }); - - // sort them even though most won't be used - const tankSortedHistory: Record = {}; - const tanks: number[] = []; - history.forEach((node) => { - if (!tankSortedHistory[node.tank_id]) { - tankSortedHistory[node.tank_id] = [node]; - tanks.push(node.tank_id); - } else { - tankSortedHistory[node.tank_id].push(node); - } - }); - - // find the ones played within the range - const inRangeTanks = tanks.filter((tankId) => - tankSortedHistory[tankId].some( - (node) => node.last_battle_time > start && node.last_battle_time < end - ) - ); - const playedTanks: number[] = []; - - // fetch history node right before "start" and right before "end" - const diff: Record = {}; - inRangeTanks.forEach((tankId) => { - const sortedHistory = tankSortedHistory[tankId]; - const previousIndex = - sortedHistory.findIndex((node) => node.last_battle_time > start) - 1; - const latestIndex = - sortedHistory.findIndex((node) => node.last_battle_time > end) - 1; - const previous = sortedHistory[previousIndex] ?? emptyTankHistoryNode; - const latest = - sortedHistory[latestIndex] ?? sortedHistory[sortedHistory.length - 1]; - - function d(get: (allStats: BlitzStats) => number) { - return get(latest.all) - get(previous.all); - } - - // check if there was a change in battles as games in rating do update last_battle_time - if (d((a) => a.battles) > 0) { - diff[tankId] = { - battles: d((a) => a.battles), - capture_points: d((a) => a.capture_points), - damage_dealt: d((a) => a.damage_dealt), - damage_received: d((a) => a.damage_received), - dropped_capture_points: d((a) => a.dropped_capture_points), - frags: d((a) => a.frags), - frags8p: d((a) => a.frags8p), - hits: d((a) => a.hits), - losses: d((a) => a.losses), - max_frags: latest.all.max_frags, - max_xp: latest.all.max_xp, - shots: d((a) => a.shots), - spotted: d((a) => a.spotted), - survived_battles: d((a) => a.survived_battles), - win_and_survived: d((a) => a.win_and_survived), - wins: d((a) => a.wins), - xp: d((a) => a.xp), - }; - - playedTanks.push(tankId); - } - }); - - const order = playedTanks.sort((a, b) => { - const aHistories = tankSortedHistory[a]; - const bHistories = tankSortedHistory[b]; - const aLatest = aHistories[aHistories.length - 1]; - const bLatest = bHistories[bHistories.length - 1]; - - return bLatest.last_battle_time - aLatest.last_battle_time; - }); - - return { diff, order } satisfies DiffedTankStats; -} diff --git a/packages/bot/src/core/blitzstars/getTankHistories.ts b/packages/bot/src/core/blitzstars/getTankHistories.ts deleted file mode 100644 index a8cbb9f95..000000000 --- a/packages/bot/src/core/blitzstars/getTankHistories.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { type Region, emptyAllStats, getTankStats } from "@blitzkit/core"; -import { Locale } from "discord.js"; -import { - type GetHistoriesOptions, - type Histories, - type History, - getHistoriesDefaultOptions, -} from "../../types/histories"; -import { UserError } from "../blitzkit/userError"; -import { translator } from "../localization/translator"; - -export interface TankHistory extends History { - tank_id: number; -} - -export type TankHistories = TankHistory[]; - -export interface TankHistoryRaw extends TankHistory { - mark_of_mastery: number; - battle_life_time: number; - account_id: number; -} - -export type TankHistoriesRaw = TankHistoryRaw[]; - -export interface GetTankHistoriesOptions extends GetHistoriesOptions { - tankId?: number; -} - -const getTankHistoriesDefaultOptions: GetTankHistoriesOptions = { - ...getHistoriesDefaultOptions, - tankId: undefined, -}; - -export async function getTankHistories( - server: Region, - id: number, - locale: Locale, - options?: Partial -) { - const { strings } = translator(locale); - const mergedOptions = { ...getTankHistoriesDefaultOptions, ...options }; - const tankHistoriesResponse = await fetch( - `https://www.blitzstars.com/api/tankhistories/for/${id}/` - ); - const tankHistories = ( - (await tankHistoriesResponse.json()) as TankHistoriesRaw - ).map( - (history) => - ({ - all: history.all, - last_battle_time: history.last_battle_time, - tank_id: history.tank_id, - }) satisfies TankHistory - ); - const compliantTanks: number[] = []; - const lastBattles: Record = {}; - const lastNonCompliantIndex: Record = {}; - const hasCompliedOnce: Record = {}; - const filtered = tankHistories.filter( - ({ last_battle_time, tank_id, all: { battles } }, index) => { - const complies = - last_battle_time >= mergedOptions.start && - last_battle_time <= mergedOptions.end && - (mergedOptions.tankId === undefined || - tank_id === mergedOptions.tankId); - - if (complies && !hasCompliedOnce[tank_id]) { - hasCompliedOnce[tank_id] = true; - compliantTanks.push(tank_id); - } - if (!hasCompliedOnce[tank_id]) lastNonCompliantIndex[tank_id] = index; - lastBattles[tank_id] = battles; - - return complies; - } - ); - - const latestNodes: TankHistories = []; - - if (mergedOptions.includeLatestHistories) { - const tankStats = await getTankStats(server, id); - - if (tankStats === null) { - throw new UserError(strings.bot.common.errors.no_tank_stats); - } - - tankStats.forEach((tankHistory) => { - if ( - tankHistory.all.battles > (lastBattles[tankHistory.tank_id] ?? 0) && - (mergedOptions.tankId === undefined || - tankHistory.tank_id === mergedOptions.tankId) - ) { - if (!hasCompliedOnce[tankHistory.tank_id]) { - // was played today for the first time... I think??? - hasCompliedOnce[tankHistory.tank_id] = true; - compliantTanks.push(tankHistory.tank_id); - } - latestNodes.push(tankHistory); - } - }); - } - - const previousNodes: TankHistories = []; - - if (mergedOptions.includePreviousHistories) { - compliantTanks.forEach((tankId) => { - if (tankId in lastNonCompliantIndex) { - previousNodes.push( - tankHistories[lastNonCompliantIndex[tankId]] ?? - ({ - all: emptyAllStats, - last_battle_time: -Infinity, - tank_id: tankId, - } satisfies TankHistory) - ); - } - }); - } - - return [...previousNodes, ...filtered, ...latestNodes] satisfies Histories; -} diff --git a/packages/bot/src/core/db/flagUserActivity.ts b/packages/bot/src/core/db/flagUserActivity.ts deleted file mode 100644 index 4fa958410..000000000 --- a/packages/bot/src/core/db/flagUserActivity.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { usersDatabase } from './users'; - -export async function flagUserActivity(blitzId: number) { - await usersDatabase.user.upsert({ - where: { blitz_id: blitzId }, - update: {}, - create: { blitz_id: blitzId }, - }); -} diff --git a/packages/bot/src/core/db/linkBlitzAndDiscord.ts b/packages/bot/src/core/db/linkBlitzAndDiscord.ts deleted file mode 100644 index 229aaeb1c..000000000 --- a/packages/bot/src/core/db/linkBlitzAndDiscord.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { flagUserActivity } from './flagUserActivity'; -import { usersDatabase } from './users'; - -export async function linkBlitzAndDiscord(discordId: bigint, blitzId: number) { - await usersDatabase.discordUser.upsert({ - where: { discord_id: discordId }, - update: { blitz_id: blitzId }, - create: { discord_id: discordId, blitz_id: blitzId }, - }); - await flagUserActivity(blitzId); -} diff --git a/packages/bot/src/core/db/users.ts b/packages/bot/src/core/db/users.ts deleted file mode 100644 index bfa600844..000000000 --- a/packages/bot/src/core/db/users.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { PrismaClient } from '../../prisma'; - -export const usersDatabase = new PrismaClient(); diff --git a/packages/bot/src/core/discord/addClanChoices.ts b/packages/bot/src/core/discord/addClanChoices.ts deleted file mode 100644 index 328ee5c34..000000000 --- a/packages/bot/src/core/discord/addClanChoices.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Locale, SlashCommandStringOption } from 'discord.js'; -import { translator } from '../localization/translator'; -import { localizationObject } from './localizationObject'; - -export function addClanChoices(option: SlashCommandStringOption) { - const { strings } = translator(Locale.EnglishUS); - - return option - .setName('clan') - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.common.options.clan.name, - undefined, - true, - ), - ) - .setDescription(strings.bot.common.options.clan.description) - .setDescriptionLocalizations( - localizationObject( - (strings) => strings.bot.common.options.clan.description, - ), - ) - .setAutocomplete(true) - .setMinLength(2) - .setRequired(true); -} diff --git a/packages/bot/src/core/discord/addFilterOptions.ts b/packages/bot/src/core/discord/addFilterOptions.ts deleted file mode 100644 index 7e1efd606..000000000 --- a/packages/bot/src/core/discord/addFilterOptions.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { - TANK_CLASSES, - TANK_TYPE_COMMAND_NAMES, - TANK_TYPES, -} from '@blitzkit/core'; -import { - type APIApplicationCommandOptionChoice, - Locale, - SlashCommandBuilder, - SlashCommandSubcommandBuilder, -} from 'discord.js'; -import { translator } from '../localization/translator'; -import { addTankChoices } from './addTankChoices'; -import { addTierChoices } from './addTierChoices'; -import { localizationObject } from './localizationObject'; - -export function addFilterOptions< - Option extends SlashCommandBuilder | SlashCommandSubcommandBuilder, ->(option: Option, nations: string[]) { - const { strings } = translator(Locale.EnglishUS); - - return ( - option - .addStringOption((option) => - option - .setName(strings.bot.common.options.filters.nation.name) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.common.options.filters.nation.name, - undefined, - true, - ), - ) - .setDescription(strings.bot.common.options.filters.nation.description) - .setDescriptionLocalizations( - localizationObject( - (strings) => - strings.bot.common.options.filters.nation.description, - ), - ) - .addChoices( - ...nations.map( - (nation) => - ({ - value: nation, - name: (strings.common.nations as Record)[ - nation - ], - name_localizations: localizationObject( - (strings) => - (strings.common.nations as Record)[ - nation - ], - ), - }) satisfies APIApplicationCommandOptionChoice, - ), - ) - .setRequired(false), - ) - .addStringOption((option) => addTierChoices(option).setRequired(false)) - .addStringOption((option) => - option - .setName(strings.bot.common.options.filters.class.name) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.common.options.filters.class.name, - undefined, - true, - ), - ) - .setDescription(strings.bot.common.options.filters.class.description) - .setDescriptionLocalizations( - localizationObject( - (strings) => strings.bot.common.options.filters.class.description, - ), - ) - .addChoices( - ...TANK_CLASSES.map( - (tankType) => - ({ - value: `${tankType}`, - name: strings.common.tank_class_short[tankType], - name_localizations: localizationObject( - (strings) => strings.common.tank_class_short[tankType], - ), - }) satisfies APIApplicationCommandOptionChoice, - ), - ) - .setRequired(false), - ) - .addStringOption((option) => - option - .setName(strings.bot.common.options.filters.type.name) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.common.options.filters.type.name, - undefined, - true, - ), - ) - .setDescription(strings.bot.common.options.filters.type.description) - .setDescriptionLocalizations( - localizationObject( - (strings) => strings.bot.common.options.filters.type.description, - ), - ) - .addChoices( - ...TANK_TYPES.map( - (treeType) => - ({ - value: TANK_TYPE_COMMAND_NAMES[treeType], - name: strings.common.tree_type[treeType], - name_localizations: localizationObject( - (strings) => strings.common.tree_type[treeType], - ), - }) satisfies APIApplicationCommandOptionChoice, - ), - ) - .setRequired(false), - ) - // TODO: somehow localize this? - .addStringOption((option) => - addTankChoices(option).setRequired(false), - ) as Option - ); -} diff --git a/packages/bot/src/core/discord/addPeriodSubCommands.ts b/packages/bot/src/core/discord/addPeriodSubCommands.ts deleted file mode 100644 index 94cf335d4..000000000 --- a/packages/bot/src/core/discord/addPeriodSubCommands.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { - Locale, - SlashCommandBuilder, - SlashCommandSubcommandBuilder, - SlashCommandSubcommandGroupBuilder, -} from 'discord.js'; -import { translator } from '../localization/translator'; -import { localizationObject } from './localizationObject'; - -export type PeriodType = 'today' | '30' | '60' | '90' | 'career' | 'custom'; -export type PeriodSize = 'today' | `${number}` | 'career'; - -export function addPeriodSubCommands( - option: SlashCommandBuilder | SlashCommandSubcommandGroupBuilder, - extra: ( - option: SlashCommandSubcommandBuilder, - ) => SlashCommandSubcommandBuilder = (option) => option, -) { - const { strings } = translator(Locale.EnglishUS); - const subcommandNameLocalizations = { - today: localizationObject( - (strings) => strings.bot.common.subcommands.today.name, - undefined, - true, - ), - 30: localizationObject( - (strings) => strings.bot.common.subcommands[30].name, - undefined, - true, - ), - 60: localizationObject( - (strings) => strings.bot.common.subcommands[60].name, - undefined, - true, - ), - 90: localizationObject( - (strings) => strings.bot.common.subcommands[90].name, - undefined, - true, - ), - career: localizationObject( - (strings) => strings.bot.common.subcommands.career.name, - undefined, - true, - ), - custom: localizationObject( - (strings) => strings.bot.common.subcommands.custom.name, - undefined, - true, - ), - }; - const subcommandDescriptionLocalizations = { - today: localizationObject( - (strings) => strings.bot.common.subcommands.today.description, - ), - 30: localizationObject( - (strings) => strings.bot.common.subcommands[30].description, - ), - 60: localizationObject( - (strings) => strings.bot.common.subcommands[60].description, - ), - 90: localizationObject( - (strings) => strings.bot.common.subcommands[90].description, - ), - career: localizationObject( - (strings) => strings.bot.common.subcommands.career.description, - ), - custom: localizationObject( - (strings) => strings.bot.common.subcommands.custom.description, - ), - }; - - option - .addSubcommand((option) => - extra( - option - .setName(strings.bot.common.subcommands.today.name) - .setNameLocalizations(subcommandNameLocalizations.today) - .setDescription(strings.bot.common.subcommands.today.description) - .setDescriptionLocalizations( - subcommandDescriptionLocalizations.today, - ), - ), - ) - .addSubcommand((option) => - extra( - option - .setName(strings.bot.common.subcommands[30].name) - .setNameLocalizations(subcommandNameLocalizations[30]) - .setDescription(strings.bot.common.subcommands[30].description) - .setDescriptionLocalizations(subcommandDescriptionLocalizations[30]), - ), - ) - .addSubcommand((option) => - extra( - option - .setName(strings.bot.common.subcommands[60].name) - .setNameLocalizations(subcommandNameLocalizations[60]) - .setDescription(strings.bot.common.subcommands[60].description) - .setDescriptionLocalizations(subcommandDescriptionLocalizations[60]), - ), - ) - .addSubcommand((option) => - extra( - option - .setName(strings.bot.common.subcommands[90].name) - .setNameLocalizations(subcommandNameLocalizations[90]) - .setDescription(strings.bot.common.subcommands[90].description) - .setDescriptionLocalizations(subcommandDescriptionLocalizations[90]), - ), - ) - .addSubcommand((option) => - extra( - option - .setName(strings.bot.common.subcommands.career.name) - .setNameLocalizations(subcommandNameLocalizations.career) - .setDescription(strings.bot.common.subcommands.career.description) - .setDescriptionLocalizations( - subcommandDescriptionLocalizations.career, - ), - ), - ) - .addSubcommand((option) => - extra( - option - .setName(strings.bot.common.subcommands.custom.name) - .setNameLocalizations(subcommandNameLocalizations.custom) - .setDescription(strings.bot.common.subcommands.custom.description) - .setDescriptionLocalizations( - subcommandDescriptionLocalizations.custom, - ) - .addIntegerOption((option) => - option - .setName(strings.bot.common.subcommands.custom.options.start.name) - .setDescription( - strings.bot.common.subcommands.custom.options.start.description, - ) - .setRequired(true), - ) - .addIntegerOption((option) => - option - .setName(strings.bot.common.subcommands.custom.options.end.name) - .setDescription( - strings.bot.common.subcommands.custom.options.end.description, - ) - .setRequired(true), - ), - ), - ); -} diff --git a/packages/bot/src/core/discord/addPeriodicFilterOptions.ts b/packages/bot/src/core/discord/addPeriodicFilterOptions.ts deleted file mode 100644 index 8e9d2f04b..000000000 --- a/packages/bot/src/core/discord/addPeriodicFilterOptions.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { - SlashCommandBuilder, - SlashCommandSubcommandBuilder, - SlashCommandSubcommandGroupBuilder, -} from 'discord.js'; -import { gameDefinitions } from '../blitzkit/nonBlockingPromises'; -import { addFilterOptions } from './addFilterOptions'; -import { addPeriodSubCommands } from './addPeriodSubCommands'; - -export async function addPeriodicFilterOptions< - OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder, ->( - option: OptionType, - extra: ( - option: SlashCommandSubcommandBuilder, - ) => SlashCommandSubcommandBuilder = (option) => option, -): Promise { - const awaitedGameDefinitions = await gameDefinitions; - - addPeriodSubCommands(option, (option) => - addFilterOptions(extra(option), awaitedGameDefinitions.nations), - ); - - return option; -} diff --git a/packages/bot/src/core/discord/addRegionChoices.ts b/packages/bot/src/core/discord/addRegionChoices.ts deleted file mode 100644 index 38adf10c9..000000000 --- a/packages/bot/src/core/discord/addRegionChoices.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { REGIONS } from "@blitzkit/core"; -import { - type APIApplicationCommandOptionChoice, - Locale, - SlashCommandStringOption, -} from "discord.js"; -import { translator } from "../localization/translator"; -import { localizationObject } from "./localizationObject"; - -export function addRegionChoices(option: SlashCommandStringOption) { - const { strings } = translator(Locale.EnglishUS); - - return option - .setName(strings.bot.common.options.region.name) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.common.options.region.name, - undefined, - true - ) - ) - .setDescription(strings.bot.common.options.region.description) - .setDescriptionLocalizations( - localizationObject( - (strings) => strings.bot.common.options.region.description - ) - ) - .addChoices( - ...REGIONS.map( - (region) => - ({ - value: region, - name: strings.common.regions.normal[region], - name_localizations: localizationObject( - (strings) => strings.common.regions.normal[region] - ), - }) satisfies APIApplicationCommandOptionChoice - ) - ) - .setRequired(true); -} diff --git a/packages/bot/src/core/discord/addTankChoices.ts b/packages/bot/src/core/discord/addTankChoices.ts deleted file mode 100644 index 3b38e220b..000000000 --- a/packages/bot/src/core/discord/addTankChoices.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Locale, SlashCommandStringOption } from "discord.js"; -import { translator } from "../localization/translator"; -import { localizationObject } from "./localizationObject"; - -export function addTankChoices(option: SlashCommandStringOption) { - const { strings } = translator(Locale.EnglishUS); - - return option - .setName(strings.bot.common.options.tank.name!) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.common.options.tank.name, - undefined, - true, - ), - ) - .setDescription(strings.bot.common.options.tank.description) - .setDescriptionLocalizations( - localizationObject( - (strings) => strings.bot.common.options.tank.description, - ), - ) - .setAutocomplete(true) - .setRequired(true); -} diff --git a/packages/bot/src/core/discord/addTierChoices.ts b/packages/bot/src/core/discord/addTierChoices.ts deleted file mode 100644 index 40ac51671..000000000 --- a/packages/bot/src/core/discord/addTierChoices.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { numberToRomanNumeral } from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { - APIApplicationCommandOptionChoice, - Locale, - SlashCommandStringOption, -} from "discord.js"; -import { range } from "lodash-es"; -import { translator } from "../localization/translator"; -import { localizationObject } from "./localizationObject"; - -export function addTierChoices(option: SlashCommandStringOption) { - const { strings } = translator(Locale.EnglishUS); - - return option - .setName(strings.bot.common.options.tier.name) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.common.options.tier.name, - undefined, - true - ) - ) - .setDescription(strings.bot.common.options.tier.description) - .setDescriptionLocalizations( - localizationObject( - (strings) => strings.bot.common.options.tier.description - ) - ) - .setChoices( - ...range(10, 0).map( - (tier) => - ({ - name: literals(strings.bot.common.options.tier.choices.tier, { - tier_1: tier, - tier_2: numberToRomanNumeral(tier), - }), - value: `${tier}`, - name_localizations: localizationObject( - (strings) => strings.bot.common.options.tier.choices.tier, - { tier_1: tier, tier_2: numberToRomanNumeral(tier) } - ), - }) satisfies APIApplicationCommandOptionChoice - ) - ) - .setRequired(true); -} diff --git a/packages/bot/src/core/discord/addUsernameChoices.ts b/packages/bot/src/core/discord/addUsernameChoices.ts deleted file mode 100644 index 35d851fdd..000000000 --- a/packages/bot/src/core/discord/addUsernameChoices.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Locale, SlashCommandStringOption } from 'discord.js'; -import { translator } from '../localization/translator'; -import { localizationObject } from './localizationObject'; - -export function addUsernameChoices(option: SlashCommandStringOption) { - const { strings } = translator(Locale.EnglishUS); - - return option - .setName(strings.bot.common.options.username.name) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.common.options.username.name, - undefined, - true, - ), - ) - .setDescription(strings.bot.common.options.username.description) - .setDescriptionLocalizations( - localizationObject( - (strings) => strings.bot.common.options.username.description, - ), - ) - .setAutocomplete(true); -} diff --git a/packages/bot/src/core/discord/autocompleteClan/constants.ts b/packages/bot/src/core/discord/autocompleteClan/constants.ts deleted file mode 100644 index 26b7f01f6..000000000 --- a/packages/bot/src/core/discord/autocompleteClan/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const DISCORD_CHOICES_MAX_NAME_SIZE = 25; -export const OVERFLOW_SUFFIX = '…'; diff --git a/packages/bot/src/core/discord/autocompleteClan/index.ts b/packages/bot/src/core/discord/autocompleteClan/index.ts deleted file mode 100644 index 5ed5d8d3f..000000000 --- a/packages/bot/src/core/discord/autocompleteClan/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { searchClansAcrossRegions } from '@blitzkit/core'; -import { - ApplicationCommandOptionChoiceData, - AutocompleteInteraction, - CacheType, -} from 'discord.js'; -import { translator } from '../../localization/translator'; -import { DISCORD_CHOICES_MAX_NAME_SIZE, OVERFLOW_SUFFIX } from './constants'; - -export async function autocompleteClan( - interaction: AutocompleteInteraction, -) { - const { strings } = translator(interaction.locale); - const focusedOption = interaction.options.getFocused(true); - if (focusedOption.name !== 'clan') return; - if (focusedOption.value.length < 2) return interaction.respond([]); - const clans = await searchClansAcrossRegions(focusedOption.value); - - await interaction.respond( - clans - ? clans.map((clan) => { - let name = `${clan.name} (${strings.common.regions.short[clan.region]})`; - - if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { - const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; - - name = `${clan.name.slice( - 0, - clan.name.length - overSize - OVERFLOW_SUFFIX.length, - )}${OVERFLOW_SUFFIX} (${strings.common.regions.short[clan.region]})`; - } - - return { - name, - value: `${clan.region}/${clan.clan_id}`, - } satisfies ApplicationCommandOptionChoiceData; - }) - : [], - ); -} diff --git a/packages/bot/src/core/discord/autocompleteTanks.ts b/packages/bot/src/core/discord/autocompleteTanks.ts deleted file mode 100644 index e43c975fb..000000000 --- a/packages/bot/src/core/discord/autocompleteTanks.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { fetchTankNames, SEARCH_KEYS, TankType } from "@blitzkit/core"; -import { - type ApplicationCommandOptionChoiceData, - AutocompleteInteraction, - type CacheType, -} from "discord.js"; -import { go } from "fuzzysort"; -import { tankNames } from "../blitzkit/nonBlockingPromises"; -import { translator } from "../localization/translator"; -import { - DISCORD_CHOICES_MAX_NAME_SIZE, - OVERFLOW_SUFFIX, -} from "./autocompleteClan/constants"; - -export const tankNamesTechTreeOnly = fetchTankNames().then((names) => - names.filter((tank) => tank.treeType === TankType.TANK_TYPE_RESEARCHABLE), -); - -export async function autocompleteTanks( - interaction: AutocompleteInteraction, - techTreeOnly = false, - fields = ["tank"], -) { - const { unwrap } = translator(interaction.locale); - const focusedOption = interaction.options.getFocused(true); - if (!fields.includes(focusedOption.name)) return; - - await interaction.respond( - focusedOption.value - ? await Promise.all( - go( - focusedOption.value, - await (techTreeOnly ? tankNamesTechTreeOnly : tankNames), - { keys: SEARCH_KEYS, limit: 10 }, - ).map(async (item) => { - let name = unwrap(item.obj.searchableName); - - if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { - const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; - - name = `${unwrap(item.obj.searchableName).slice( - 0, - unwrap(item.obj.searchableName).length - - overSize - - OVERFLOW_SUFFIX.length, - )}${OVERFLOW_SUFFIX}`; - } - - return { - name, - value: `${item.obj.id}`, - } satisfies ApplicationCommandOptionChoiceData; - }), - ) - : [], - ); -} diff --git a/packages/bot/src/core/discord/autocompleteUsername.ts b/packages/bot/src/core/discord/autocompleteUsername.ts deleted file mode 100644 index 522721c84..000000000 --- a/packages/bot/src/core/discord/autocompleteUsername.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { searchPlayersAcrossRegions } from '@blitzkit/core'; -import { - ApplicationCommandOptionChoiceData, - AutocompleteInteraction, - CacheType, -} from 'discord.js'; -import { translator } from '../localization/translator'; -import { - DISCORD_CHOICES_MAX_NAME_SIZE, - OVERFLOW_SUFFIX, -} from './autocompleteClan/constants'; - -export async function autocompleteUsername( - interaction: AutocompleteInteraction, -) { - const { strings } = translator(interaction.locale); - const focusedOption = interaction.options.getFocused(true); - if (focusedOption.name !== 'username') return; - const players = await searchPlayersAcrossRegions(focusedOption.value); - - try { - await interaction.respond( - players - ? players.map((player) => { - let name = `${player.nickname} (${ - strings.common.regions.short[player.region] - })`; - - if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) { - const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE; - - name = `${player.nickname.slice( - 0, - player.nickname.length - overSize - OVERFLOW_SUFFIX.length, - )}${OVERFLOW_SUFFIX} (${ - strings.common.regions.short[player.region] - })`; - } - - return { - name, - value: `${player.account_id}`, - } satisfies ApplicationCommandOptionChoiceData; - }) - : [], - ); - } catch (error) {} -} diff --git a/packages/bot/src/core/discord/buttonLink.ts b/packages/bot/src/core/discord/buttonLink.ts deleted file mode 100644 index 65751dd57..000000000 --- a/packages/bot/src/core/discord/buttonLink.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - ButtonBuilder, - ButtonStyle, - type ComponentEmojiResolvable, -} from "discord.js"; - -export function buttonLink( - url: string, - label: string, - emoji?: ComponentEmojiResolvable -) { - const button = new ButtonBuilder() - .setStyle(ButtonStyle.Link) - .setURL(url) - .setLabel(label); - - if (emoji) button.setEmoji(emoji); - - return button; -} diff --git a/packages/bot/src/core/discord/buttonPrimary.ts b/packages/bot/src/core/discord/buttonPrimary.ts deleted file mode 100644 index 2fa4733a0..000000000 --- a/packages/bot/src/core/discord/buttonPrimary.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - ButtonBuilder, - ButtonStyle, - type ComponentEmojiResolvable, -} from "discord.js"; - -export function buttonPrimary( - id: string, - label: string, - emoji?: ComponentEmojiResolvable -) { - const button = new ButtonBuilder() - .setCustomId(id) - .setStyle(ButtonStyle.Primary) - .setLabel(label); - - if (emoji) button.setEmoji(emoji); - - return button; -} diff --git a/packages/bot/src/core/discord/buttonRefresh.ts b/packages/bot/src/core/discord/buttonRefresh.ts deleted file mode 100644 index fdf21e961..000000000 --- a/packages/bot/src/core/discord/buttonRefresh.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ChatInputCommandInteraction } from 'discord.js'; -import { translator } from '../localization/translator'; -import { buttonPrimary } from './buttonPrimary'; - -export function buttonRefresh( - interaction: ChatInputCommandInteraction, - path: string, -) { - const { strings } = translator(interaction.locale); - - if ( - interaction.appPermissions?.has('ReadMessageHistory') && - interaction.appPermissions?.has('ViewChannel') - ) { - return buttonPrimary(path, strings.bot.common.buttons.refresh); - } - - return null; -} diff --git a/packages/bot/src/core/discord/chunkLines.ts b/packages/bot/src/core/discord/chunkLines.ts deleted file mode 100644 index dfcdef6d0..000000000 --- a/packages/bot/src/core/discord/chunkLines.ts +++ /dev/null @@ -1,17 +0,0 @@ -export function chunkLines(lines: string[], joiner = '\n', maxSize = 2000) { - const chunks = ['']; - - lines.map((line) => { - if (chunks.at(-1)!.length + line.length > maxSize) { - chunks.push(''); - } - - if (chunks.at(-1)!.length === 0) { - chunks[chunks.length - 1] += line; - } else { - chunks[chunks.length - 1] += joiner + line; - } - }); - - return chunks; -} diff --git a/packages/bot/src/core/discord/client.ts b/packages/bot/src/core/discord/client.ts deleted file mode 100644 index a83fb3822..000000000 --- a/packages/bot/src/core/discord/client.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Client, GatewayIntentBits } from "discord.js"; -import { interactionCreate } from "../../events/interactionCreate"; -import { ready } from "../../events/ready"; - -export const client = new Client({ - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers], -}) - .on("clientReady", ready) - .on("interactionCreate", interactionCreate); diff --git a/packages/bot/src/core/discord/colors.ts b/packages/bot/src/core/discord/colors.ts deleted file mode 100644 index bb2c7f51d..000000000 --- a/packages/bot/src/core/discord/colors.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const ACCENT_COLOR = '#ecedee'; -export const POSITIVE_COLOR = '#82ff29'; -export const NEGATIVE_COLOR = '#ff4747'; -export const WARNING_COLOR = '#ffab00'; diff --git a/packages/bot/src/core/discord/commandToURL.ts b/packages/bot/src/core/discord/commandToURL.ts deleted file mode 100644 index 91455adeb..000000000 --- a/packages/bot/src/core/discord/commandToURL.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { type CacheType, ChatInputCommandInteraction } from "discord.js"; - -export interface Parameters { - [key: string]: string | number | boolean | undefined | null; -} - -// TODO: automatically infer command options??? -// but how would that work with the region and id options? - -export function commandToURL( - interaction: ChatInputCommandInteraction, - parameters?: Parameters -) { - const pathname = [ - interaction.commandName, - interaction.options.getSubcommandGroup(false), - interaction.options.getSubcommand(false), - ] - .filter(Boolean) - .join("/"); - const params = (parameters ? Object.entries(parameters) : []) - .filter(([, value]) => value !== undefined && value !== null) - .map(([key, value]) => `${key}=${value}`) - .join("&"); - - return `${pathname}${params.length > 0 ? `?${params}` : ""}`; -} diff --git a/packages/bot/src/core/discord/createLocalizedCommand.ts b/packages/bot/src/core/discord/createLocalizedCommand.ts deleted file mode 100644 index acb00ca49..000000000 --- a/packages/bot/src/core/discord/createLocalizedCommand.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { type BlitzKitStrings } from "@blitzkit/i18n"; -import { - Locale, - SlashCommandBuilder, - SlashCommandSubcommandBuilder, - SlashCommandSubcommandGroupBuilder, -} from "discord.js"; -import { translator } from "../localization/translator"; -import { localizationObject } from "./localizationObject"; - -interface CreateLocalizedCommandSubcommand { - subcommand: string; - modify?: (option: SlashCommandSubcommandBuilder) => void; -} - -interface CreateLocalizedCommandSubcommandGroup { - group: string; - modify?: (option: SlashCommandSubcommandGroupBuilder) => void; -} - -export function createLocalizedCommand( - command: string, - sub?: - | CreateLocalizedCommandSubcommand[] - | CreateLocalizedCommandSubcommandGroup[], -) { - const commandPathItem = command.replaceAll( - "-", - "_", - ) as keyof BlitzKitStrings["bot"]["commands"]; - const { strings } = translator(Locale.EnglishUS); - const slashCommand = new SlashCommandBuilder() - .setName(command) - .setNameLocalizations( - localizationObject( - (strings) => strings.bot.commands[commandPathItem].name, - undefined, - true, - ), - ) - .setDescription(strings.bot.commands[commandPathItem].description) - .setDescriptionLocalizations( - localizationObject( - (strings) => strings.bot.commands[commandPathItem].description, - ), - ); - - if (sub) { - sub.map((subItem) => { - if ("subcommand" in subItem) { - const subcommandNameLocalizations = localizationObject( - (strings) => - ( - strings.bot.commands[commandPathItem] as unknown as { - subcommands: Record; - } - ).subcommands[subItem.subcommand].name, - undefined, - true, - ); - const subcommandDescriptionLocalizations = localizationObject( - (strings) => - ( - strings.bot.commands[commandPathItem] as unknown as { - subcommands: Record; - } - ).subcommands[subItem.subcommand].description, - ); - - slashCommand.addSubcommand((option) => { - if (subItem.modify) subItem.modify(option); - - return option - .setName(subItem.subcommand) - .setNameLocalizations(subcommandNameLocalizations) - .setDescription( - ( - strings.bot.commands[commandPathItem] as unknown as { - subcommands: Record; - } - ).subcommands[subItem.subcommand].description, - ) - .setDescriptionLocalizations(subcommandDescriptionLocalizations); - }); - } else { - const subcommandNameLocalizations = localizationObject( - (strings) => - ( - strings.bot.commands[commandPathItem] as unknown as { - groups: Record; - } - ).groups[subItem.group].name, - undefined, - true, - ); - const subcommandDescriptionLocalizations = localizationObject( - (strings) => - ( - strings.bot.commands[commandPathItem] as unknown as { - groups: Record; - } - ).groups[subItem.group].description, - ); - - slashCommand.addSubcommandGroup((option) => { - if (subItem.modify) subItem.modify(option); - - return option - .setName(subItem.group) - .setNameLocalizations(subcommandNameLocalizations) - .setDescription( - ( - strings.bot.commands[commandPathItem] as unknown as { - groups: Record; - } - ).groups[subItem.group].description, - ) - .setDescriptionLocalizations(subcommandDescriptionLocalizations); - }); - } - }); - } - - return slashCommand; -} diff --git a/packages/bot/src/core/discord/embedInfo.ts b/packages/bot/src/core/discord/embedInfo.ts deleted file mode 100644 index 5359e3212..000000000 --- a/packages/bot/src/core/discord/embedInfo.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { ACCENT_COLOR } from './colors'; - -export function embedInfo(title: string, description: string) { - const embed = new EmbedBuilder() - .setColor(ACCENT_COLOR) - .setDescription(description); - - if (title) embed.setTitle(title); - - return embed; -} diff --git a/packages/bot/src/core/discord/embedNegative.ts b/packages/bot/src/core/discord/embedNegative.ts deleted file mode 100644 index 1d520500a..000000000 --- a/packages/bot/src/core/discord/embedNegative.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { NEGATIVE_COLOR } from './colors'; - -export function embedNegative(title: string, description: string) { - return new EmbedBuilder() - .setColor(NEGATIVE_COLOR) - .setTitle(title) - .setDescription(description); -} diff --git a/packages/bot/src/core/discord/embedPositive.ts b/packages/bot/src/core/discord/embedPositive.ts deleted file mode 100644 index 5f157a851..000000000 --- a/packages/bot/src/core/discord/embedPositive.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { POSITIVE_COLOR } from './colors'; - -export function embedPositive(title: string, description: string) { - return new EmbedBuilder() - .setColor(POSITIVE_COLOR) - .setTitle(title) - .setDescription(description); -} diff --git a/packages/bot/src/core/discord/embedWarning.ts b/packages/bot/src/core/discord/embedWarning.ts deleted file mode 100644 index 0cb912d9e..000000000 --- a/packages/bot/src/core/discord/embedWarning.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; -import { WARNING_COLOR } from './colors'; - -export function embedWarning(title: string, description: string) { - return new EmbedBuilder() - .setColor(WARNING_COLOR) - .setTitle(title) - .setDescription(description); -} diff --git a/packages/bot/src/core/discord/getCustomPeriodParams.ts b/packages/bot/src/core/discord/getCustomPeriodParams.ts deleted file mode 100644 index eaa19e7fa..000000000 --- a/packages/bot/src/core/discord/getCustomPeriodParams.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ChatInputCommandInteraction } from "discord.js"; -import type { PeriodType } from "./addPeriodSubCommands"; - -export function getCustomPeriodParams( - interaction: ChatInputCommandInteraction, - forcedNonCustomPeriod = false -) { - if ( - !forcedNonCustomPeriod && - (interaction.options.getSubcommand() as PeriodType) === "custom" - ) { - return { - start: interaction.options.getInteger("start", true), - end: interaction.options.getInteger("end", true), - }; - } - - return undefined; -} diff --git a/packages/bot/src/core/discord/getFiltersFromButton.ts b/packages/bot/src/core/discord/getFiltersFromButton.ts deleted file mode 100644 index 37689b7f0..000000000 --- a/packages/bot/src/core/discord/getFiltersFromButton.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { TankClass, TankType } from "@blitzkit/core"; -import { ButtonInteraction } from "discord.js"; -import type { StatFilters } from "../blitzstars/filterStats"; - -export function getFiltersFromButton(interaction: ButtonInteraction) { - const url = new URL(`https://example.com/${interaction.customId}`); - - return { - nation: url.searchParams.get("nation") ?? undefined, - tank: parseInt(url.searchParams.get("tank") ?? "0") || undefined, - class: - (Number(url.searchParams.get("class")) as TankClass | undefined) ?? - undefined, - type: - (Number(url.searchParams.get("type")) as TankType | undefined) ?? - undefined, - tier: parseInt(url.searchParams.get("tier") ?? "0") || undefined, - } satisfies StatFilters; -} diff --git a/packages/bot/src/core/discord/getFiltersFromCommand.ts b/packages/bot/src/core/discord/getFiltersFromCommand.ts deleted file mode 100644 index d4cac40cf..000000000 --- a/packages/bot/src/core/discord/getFiltersFromCommand.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ChatInputCommandInteraction } from 'discord.js'; -import { resolveTankId } from '../blitz/resolveTankId'; -import type { StatFilters } from '../blitzstars/filterStats'; - -export async function getFiltersFromCommand( - interaction: ChatInputCommandInteraction, -) { - const nationRaw = interaction.options.getString('nation'); - const tierRaw = interaction.options.getString('tier'); - const classRaw = interaction.options.getString('class'); - const typeRaw = interaction.options.getString('type'); - const tankRaw = interaction.options.getString('tank'); - - return { - nation: nationRaw ?? undefined, - tier: tierRaw === null ? undefined : Number(tierRaw), - class: classRaw === null ? undefined : Number(classRaw), - type: typeRaw === null ? undefined : Number(typeRaw), - tank: - tankRaw === null - ? undefined - : await resolveTankId(tankRaw, interaction.locale), - } satisfies StatFilters; -} diff --git a/packages/bot/src/core/discord/getPeriodOptionName.ts b/packages/bot/src/core/discord/getPeriodOptionName.ts deleted file mode 100644 index 1eaea0772..000000000 --- a/packages/bot/src/core/discord/getPeriodOptionName.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { literals } from "@blitzkit/i18n"; -import { Locale } from "discord.js"; -import { translator } from "../localization/translator"; -import type { PeriodSize } from "./addPeriodSubCommands"; - -export function getPeriodOptionName(period: PeriodSize, locale: Locale) { - const { strings } = translator(locale); - if (period === "career") return strings.bot.common.periods.career; - if (period === "today") return strings.bot.common.periods.today; - return literals(strings.bot.common.periods.days, { days: period }); -} diff --git a/packages/bot/src/core/discord/localizationObject.ts b/packages/bot/src/core/discord/localizationObject.ts deleted file mode 100644 index 637a6d604..000000000 --- a/packages/bot/src/core/discord/localizationObject.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { type BlitzKitStrings, literals as literalsFunc } from "@blitzkit/i18n"; -import { Locale } from "discord.js"; -import { SUPPORTED_LOCALES_DISCORD } from "../localization/strings/constants"; -import { translator } from "../localization/translator"; - -const validNameRegex = - /^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]+$/u; - -export function localizationObject( - path: (strings: BlitzKitStrings) => string, - literals?: Record, - strict = false -) { - return SUPPORTED_LOCALES_DISCORD.reduce>>( - (localizations, locale) => { - const { strings } = translator(locale); - const translation = literalsFunc(path(strings), literals); - const isValid = - (!strict || validNameRegex.test(translation)) && translation.length > 0; - - if (!isValid) { - // console.warn( - // `Invalid localization for ${locale} (${path}): ${translation}; skipping...` - // ); - return localizations; - } - - return { - ...localizations, - [locale]: translation, - }; - }, - {} - ); -} diff --git a/packages/bot/src/core/discord/markdownTable.ts b/packages/bot/src/core/discord/markdownTable.ts deleted file mode 100644 index 6ad0b6f64..000000000 --- a/packages/bot/src/core/discord/markdownTable.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type TableInputEntry = [string, string | number] | []; -export type TableInput = TableInputEntry[]; - -export function markdownTable(input: TableInput) { - return input - .filter((array) => array[1] !== undefined) - .map((array) => (array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`)) - .join('\n'); -} diff --git a/packages/bot/src/core/discord/normalizeInteractionReturnable.tsx b/packages/bot/src/core/discord/normalizeInteractionReturnable.tsx deleted file mode 100644 index 7e67b9767..000000000 --- a/packages/bot/src/core/discord/normalizeInteractionReturnable.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - ActionRowBuilder, - AttachmentBuilder, - ButtonBuilder, - EmbedBuilder, - type InteractionEditReplyOptions, - type InteractionReplyOptions, - type MessageEditOptions, -} from "discord.js"; -import type { InteractionReturnable } from "../../events/interactionCreate"; -import type { Writeable } from "../../types/writable"; -import { jsxToPngThreaded } from "../blitzkit/jsxToPngThreaded"; -import { RenderConfiguration } from "../blitzkit/renderConfiguration"; - -export async function normalizeInteractionReturnable( - returnable: InteractionReturnable -) { - const images: [number, Buffer][] = []; - const reply: InteractionEditReplyOptions & - InteractionReplyOptions & - MessageEditOptions = {}; - const awaitedReturnable = await returnable; - const normalizedReturnable = Array.isArray(awaitedReturnable) - ? awaitedReturnable - : [awaitedReturnable]; - let renderConfiguration = new RenderConfiguration(); - - await Promise.all( - normalizedReturnable.map(async (item, index) => { - if (typeof item === "string") { - reply.content = item; - } else if (item instanceof EmbedBuilder) { - if (!reply.embeds) reply.embeds = []; - (reply.embeds as Writeable).push(item); - } else if (item instanceof ButtonBuilder) { - if (!reply.components) - reply.components = [new ActionRowBuilder()]; - (reply.components[0] as ActionRowBuilder).addComponents( - item - ); - } else if (item instanceof AttachmentBuilder) { - if (!reply.files) reply.files = []; - (reply.files as Writeable).push(item); - } else if (item instanceof RenderConfiguration) { - renderConfiguration = item; - } else if (item === null) { - return; - } else { - const image = await jsxToPngThreaded(item, renderConfiguration); - images.push([index, image]); - } - }) - ); - - if (images.length > 0) { - images.sort(([a], [b]) => b - a); - if (!reply.files) reply.files = []; - images.forEach(([, image]) => - (reply.files as Writeable)?.push(image) - ); - } - - return reply; -} diff --git a/packages/bot/src/core/discord/psa.ts b/packages/bot/src/core/discord/psa.ts deleted file mode 100644 index 42dfe93eb..000000000 --- a/packages/bot/src/core/discord/psa.ts +++ /dev/null @@ -1,36 +0,0 @@ -export type PSAData = { - secret?: boolean; - links?: { - title: string; - url: string; - }[]; - commands?: string[]; -} & ( - | { - type?: 'embed'; - title: string; - description: string; - image?: string; - } - | { - type: 'image'; - image: string; - title?: string; - } -); - -export interface PSA { - data: PSAData | null; -} - -export const psa: PSA = { - data: null, -}; - -fetch( - 'https://gist.githubusercontent.com/tresabhi/ed4b136f08856e615212e573aabe1968/raw', -) - .then((response) => response.json()) - .then((data) => { - psa.data = data as PSAData; - }); diff --git a/packages/bot/src/core/discord/resolveClanFromCommand.ts b/packages/bot/src/core/discord/resolveClanFromCommand.ts deleted file mode 100644 index 2a78793b6..000000000 --- a/packages/bot/src/core/discord/resolveClanFromCommand.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Region } from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { ChatInputCommandInteraction } from "discord.js"; -import markdownEscape from "markdown-escape"; -import { searchClansAcrossRegionsBotWrapper } from "../blitz/searchClansAcrossRegionsBotWrapper"; -import { UserError } from "../blitzkit/userError"; -import { translator } from "../localization/translator"; - -export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/; - -export async function resolveClanFromCommand( - interaction: ChatInputCommandInteraction -) { - const { strings } = translator(interaction.locale); - 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 searchClansAcrossRegionsBotWrapper( - clan, - interaction.locale - ); - - if (accounts[0]) { - return { region: accounts[0].region, id: accounts[0].clan_id }; - } else { - throw new UserError( - literals(strings.bot.common.errors.clan_not_found, { - search: markdownEscape(clan), - }) - ); - } - } -} diff --git a/packages/bot/src/core/discord/resolvePeriodFromButton.ts b/packages/bot/src/core/discord/resolvePeriodFromButton.ts deleted file mode 100644 index cb9388bdf..000000000 --- a/packages/bot/src/core/discord/resolvePeriodFromButton.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getTimeDaysAgo, type Region } from "@blitzkit/core"; -import { ButtonInteraction } from "discord.js"; -import { getPeriodNow } from "../blitzkit/getPeriodNow"; -import { getPeriodStart } from "../blitzkit/getPeriodStart"; -import type { PeriodType } from "./addPeriodSubCommands"; -import { getPeriodOptionName } from "./getPeriodOptionName"; -import type { ResolvedPeriod } from "./resolvePeriodFromCommand"; - -export function resolvePeriodFromButton( - region: Region, - interaction: ButtonInteraction -) { - let name: string; - let start: number; - let end: number; - const url = new URL(`https://example.com/${interaction.customId}`); - const path = url.pathname.split("/"); - const periodSubcommand = path.at(-1) as PeriodType; - - if (periodSubcommand === "custom") { - const startRaw = parseInt(url!.searchParams.get("start")!); - const endRaw = parseInt(url!.searchParams.get("end")!); - const startDaysAgoMin = Math.min(startRaw, endRaw); - const endDaysAgoMax = Math.max(startRaw, endRaw); - - name = `${startDaysAgoMin} - ${endDaysAgoMax} days`; - start = getTimeDaysAgo(region, endDaysAgoMax); - end = getTimeDaysAgo(region, startDaysAgoMin); - } else { - name = getPeriodOptionName(periodSubcommand, interaction.locale); - start = getPeriodStart(region, periodSubcommand); - end = getPeriodNow(); - } - - return { - name, - start, - end, - } satisfies ResolvedPeriod; -} diff --git a/packages/bot/src/core/discord/resolvePeriodFromCommand.ts b/packages/bot/src/core/discord/resolvePeriodFromCommand.ts deleted file mode 100644 index 55ad2af4e..000000000 --- a/packages/bot/src/core/discord/resolvePeriodFromCommand.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getTimeDaysAgo, type Region } from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { type CacheType, ChatInputCommandInteraction } from "discord.js"; -import { getPeriodNow } from "../blitzkit/getPeriodNow"; -import { getPeriodStart } from "../blitzkit/getPeriodStart"; -import { translator } from "../localization/translator"; -import type { PeriodType } from "./addPeriodSubCommands"; -import { getPeriodOptionName } from "./getPeriodOptionName"; - -export interface ResolvedPeriod { - name: string; - start: number; - end: number; -} - -export function resolvePeriodFromCommand( - region: Region, - interaction: ChatInputCommandInteraction, - forcedPeriod?: PeriodType -) { - const { strings } = translator(interaction.locale); - let name: string; - let start: number; - let end: number; - const periodSubcommand = forcedPeriod - ? forcedPeriod - : (interaction.options.getSubcommand() as PeriodType); - - if (periodSubcommand === "custom") { - const startOption = interaction.options.getInteger("start", true); - const endOption = interaction.options.getInteger("end", true); - const startDaysAgoMin = Math.min(startOption, endOption); - const endDaysAgoMax = Math.max(startOption, endOption); - - name = literals(strings.bot.common.periods.custom, { - day_1: startDaysAgoMin, - day_2: endDaysAgoMax, - }); - start = getTimeDaysAgo(region, endDaysAgoMax); - end = getTimeDaysAgo(region, startDaysAgoMin); - } else { - name = getPeriodOptionName(periodSubcommand, interaction.locale); - start = getPeriodStart(region, periodSubcommand); - end = getPeriodNow(); - } - - return { - name, - start, - end, - } satisfies ResolvedPeriod; -} diff --git a/packages/bot/src/core/discord/resolvePlayerFromButton.ts b/packages/bot/src/core/discord/resolvePlayerFromButton.ts deleted file mode 100644 index 06b3e2895..000000000 --- a/packages/bot/src/core/discord/resolvePlayerFromButton.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Region } from "@blitzkit/core"; -import { ButtonInteraction } from "discord.js"; -import type { ResolvedPlayer } from "./resolvePlayerFromCommand"; - -export async function resolvePlayerFromButton(interaction: ButtonInteraction) { - const url = new URL(`https://exmaple.com/${interaction.customId}`); - - return { - id: parseInt(url.searchParams.get("id")!), - region: url.searchParams.get("region") as Region, - } satisfies ResolvedPlayer; -} diff --git a/packages/bot/src/core/discord/resolvePlayerFromCommand/index.ts b/packages/bot/src/core/discord/resolvePlayerFromCommand/index.ts deleted file mode 100644 index c76e6c70e..000000000 --- a/packages/bot/src/core/discord/resolvePlayerFromCommand/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { idToRegion, type Region } from "@blitzkit/core"; -import { literals } from "@blitzkit/i18n"; -import { type CacheType, ChatInputCommandInteraction } from "discord.js"; -import markdownEscape from "markdown-escape"; -import { searchPlayersAcrossRegionsBotWrapper } from "../../blitz/searchPlayersAcrossRegionsBotWrapper"; -import { getBlitzFromDiscord } from "../../blitzkit/getBlitzFromDiscord"; -import { UserError } from "../../blitzkit/userError"; -import { translator } from "../../localization/translator"; - -export interface ResolvedPlayer { - region: Region; - id: number; -} - -export async function resolvePlayerFromCommand( - interaction: ChatInputCommandInteraction -) { - const { strings } = translator(interaction.locale); - const commandUsername = interaction.options.getString("username"); - - if (commandUsername !== null) { - const id = parseInt(commandUsername); - - if (!isNaN(id)) { - const region = idToRegion(id); - return { region, id } satisfies ResolvedPlayer; - } else { - const accounts = await searchPlayersAcrossRegionsBotWrapper( - commandUsername, - interaction.locale - ); - - if (accounts[0]) { - return { - region: accounts[0].region, - id: accounts[0].account_id, - } satisfies ResolvedPlayer; - } else { - throw new UserError( - literals(strings.bot.common.errors.player_not_found, { - search: markdownEscape(commandUsername), - }) - ); - } - } - } else { - const account = await getBlitzFromDiscord(BigInt(interaction.user.id)); - - if (account) return account; - - throw new UserError(strings.bot.common.errors.player_not_linked); - } -} diff --git a/packages/bot/src/core/localization/strings/constants.ts b/packages/bot/src/core/localization/strings/constants.ts deleted file mode 100644 index 4cb69a89c..000000000 --- a/packages/bot/src/core/localization/strings/constants.ts +++ /dev/null @@ -1,25 +0,0 @@ -import locales from "@blitzkit/i18n/locales.json"; -import { Locale } from "discord.js"; - -const SUPPORTED_LOCALES_DISCORD_MAP: Record = { - en: Locale.EnglishUS, - es: Locale.SpanishES, - pt: Locale.PortugueseBR, - ru: Locale.Russian, - uk: Locale.Ukrainian, - zh: Locale.ChineseCN, - fr: Locale.French, - ja: Locale.Japanese, - pl: Locale.Polish, -}; - -export const SUPPORTED_LOCALES_DISCORD_MAP_INVERSE = Object.fromEntries( - Object.entries(SUPPORTED_LOCALES_DISCORD_MAP).map(([k, v]) => [v, k]) -) as Record; - -export const SUPPORTED_LOCALES_DISCORD = Object.values( - SUPPORTED_LOCALES_DISCORD_MAP -); - -export const DEFAULT_LOCALE_DISCORD = - SUPPORTED_LOCALES_DISCORD_MAP[locales.default]; diff --git a/packages/bot/src/core/localization/translator.ts b/packages/bot/src/core/localization/translator.ts deleted file mode 100644 index 37168ac30..000000000 --- a/packages/bot/src/core/localization/translator.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { STRINGS, unwrapper } from '@blitzkit/i18n'; -import { Locale } from 'discord.js'; -import { - DEFAULT_LOCALE_DISCORD, - SUPPORTED_LOCALES_DISCORD, - SUPPORTED_LOCALES_DISCORD_MAP_INVERSE, -} from './strings/constants'; - -export function translator(localeRaw: Locale) { - const locale = - localeRaw !== DEFAULT_LOCALE_DISCORD && - SUPPORTED_LOCALES_DISCORD.includes(localeRaw) - ? localeRaw - : DEFAULT_LOCALE_DISCORD; - const bkLocale = SUPPORTED_LOCALES_DISCORD_MAP_INVERSE[locale]; - const strings = STRINGS[bkLocale]; - const unwrap = unwrapper(bkLocale); - - return { locale, strings, unwrap, bkLocale }; -} diff --git a/packages/bot/src/core/radix/radixColors.ts b/packages/bot/src/core/radix/radixColors.ts deleted file mode 100644 index dfe17a359..000000000 --- a/packages/bot/src/core/radix/radixColors.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as colors from "@radix-ui/colors"; -import { Theme } from "@radix-ui/themes"; -import type { ComponentProps } from "react"; - -export type AccentColor = Exclude< - ComponentProps["accentColor"], - "gray" | undefined ->; -export type AccentColorDark = `${AccentColor}Dark`; -export type GrayColor = Exclude< - ComponentProps["grayColor"], - "auto" | undefined ->; -export type GrayColorDark = `${GrayColor}Dark`; - -export const ACCENT_COLORS: AccentColor[] = [ - "tomato", - "red", - "ruby", - "crimson", - "pink", - "plum", - "purple", - "violet", - "iris", - "indigo", - "blue", - "cyan", - "teal", - "jade", - "green", - "grass", - "brown", - "orange", - "sky", - "mint", - "lime", - "yellow", - "amber", - "gold", - "bronze", -]; -export const GRAY_COLORS: GrayColor[] = [ - "gray", - "mauve", - "slate", - "sage", - "olive", - "sand", -]; - -export const PALETTES: Record< - AccentColor | AccentColorDark | GrayColor | GrayColorDark, - { [key: `${string}${number}`]: string } -> = colors; diff --git a/packages/bot/src/core/streams/wotbreplay.ts b/packages/bot/src/core/streams/wotbreplay.ts deleted file mode 100644 index 47d18d9d3..000000000 --- a/packages/bot/src/core/streams/wotbreplay.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ReadStream } from "@blitzkit/core"; -import AdmZip from "adm-zip"; -import { readFile } from "fs/promises"; -import { Parser } from "pickleparser"; -import protobuf from "protobufjs"; - -interface WotbReplayMetaJson { - version: string; - title: string; - dbid: string; - playerName: string; - battleStartTime: string; - playerVehicleName: string; - mapName: string; - arenaUniqueId: string; - battleDuration: number; - vehicleCompDescriptor: number; - camouflageId: number; - mapId: number; - arenaBonusType: number; -} - -export class WotbReplayReadStream extends ReadStream { - async wotbReplay() { - const pickleParser = new Parser(); - const zip = new AdmZip(Buffer.from(this.buffer)); - const metaJsonBuffer = zip.getEntry("meta.json")?.getData(); - const battleResultsDat = zip.getEntry("battle_results.dat")?.getData(); - - if (!metaJsonBuffer || !battleResultsDat) - throw new SyntaxError("meta.json not found"); - - const metaJson = JSON.parse( - metaJsonBuffer.toString() - ) as WotbReplayMetaJson; - const unpickledBattleResults = pickleParser.parse(battleResultsDat); - const [, battleResultsString] = unpickledBattleResults as [number, string]; - const root = await protobuf.load( - `${__dirname}/../../protos/BattleResultsClient.proto` - ); - const BattleResults = root.lookupType( - "BattleResultsGenerated.BattleResultsClient" - ); - - const reader = protobuf.Reader.create(Buffer.from(battleResultsString)); - - // const decodedBattleResults = BattleResults.decodeDelimited( - // Buffer.from(battleResultsString), - // ); - // console.log(decodedBattleResults.toJSON()); - } -} - -const replay = await readFile("temp/test3.wotbreplay"); -const stream = new WotbReplayReadStream(replay.buffer as ArrayBuffer); - -stream.wotbReplay(); diff --git a/packages/bot/src/events/interactionCreate/handlers/autocomplete.ts b/packages/bot/src/events/interactionCreate/handlers/autocomplete.ts deleted file mode 100644 index 59ec8f072..000000000 --- a/packages/bot/src/events/interactionCreate/handlers/autocomplete.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { AutocompleteInteraction, CacheType } from "discord.js"; -import { commands } from ".."; - -export async function handleAutocomplete( - interaction: AutocompleteInteraction, -) { - const command = commands[interaction.commandName]; - - if (!command) { - console.error(`No command matching ${interaction.commandName} was found.`); - return; - } - - if (command.autocomplete) command.autocomplete(interaction); -} diff --git a/packages/bot/src/events/interactionCreate/handlers/button.tsx b/packages/bot/src/events/interactionCreate/handlers/button.tsx deleted file mode 100644 index 8ac4b7269..000000000 --- a/packages/bot/src/events/interactionCreate/handlers/button.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ButtonInteraction, CacheType } from "discord.js"; -import { commands } from ".."; -import { normalizeInteractionReturnable } from "../../../core/discord/normalizeInteractionReturnable"; - -export async function handleButton(interaction: ButtonInteraction) { - await interaction.deferUpdate(); - - const url = new URL(`https://example.com/${interaction.customId}`); - const commandName = url.pathname.split("/")[1]; - const button = commands[commandName]?.button; - - if (!button) { - throw new Error(`Button handler not found for "${commandName}"`); - } - - if (interaction.message.interaction?.user.id !== interaction.user.id) { - interaction.followUp({ - ephemeral: true, - content: "You are not the author of this command.", - }); - return; - } - - await interaction.message.edit( - await normalizeInteractionReturnable(button(interaction)), - ); -} diff --git a/packages/bot/src/events/interactionCreate/handlers/chatInputCommand.ts b/packages/bot/src/events/interactionCreate/handlers/chatInputCommand.ts deleted file mode 100644 index d385a036a..000000000 --- a/packages/bot/src/events/interactionCreate/handlers/chatInputCommand.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - ActionRowBuilder, - AttachmentBuilder, - ButtonBuilder, - ButtonStyle, - type CacheType, - ChatInputCommandInteraction, - type InteractionReplyOptions, -} from "discord.js"; -import { type InteractionRawReturnable, commands } from ".."; -import { UserError } from "../../../core/blitzkit/userError"; -import { buttonLink } from "../../../core/discord/buttonLink"; -import { embedWarning } from "../../../core/discord/embedWarning"; -import { normalizeInteractionReturnable } from "../../../core/discord/normalizeInteractionReturnable"; -import { psa } from "../../../core/discord/psa"; -import { translator } from "../../../core/localization/translator"; -import type { Writeable } from "../../../types/writable"; - -export async function handleChatInputCommand( - interaction: ChatInputCommandInteraction, -) { - await interaction.deferReply(); - - const registry = commands[interaction.commandName]; - - try { - const returnable = await registry.handler(interaction); - - if (registry.handlesInteraction) return; - - const chunks: InteractionRawReturnable[][] = [[]]; - - (Array.isArray(returnable) ? returnable : [returnable]).forEach( - (object) => { - if (typeof object === "string") { - chunks.push([object]); - } else chunks.at(-1)?.push(object!); - }, - ); - - let index = 0; - for (const chunk of chunks.filter((chunk) => chunk.length > 0)) { - const reply = await normalizeInteractionReturnable(chunk); - - if ( - psa.data && - !psa.data.secret && - (psa.data.commands === undefined || - psa.data.commands.includes(interaction.commandName)) - ) { - if (psa.data.type === undefined || psa.data.type === "embed") { - if (!reply.embeds) reply.embeds = []; - - const embed = embedWarning(psa.data.title, psa.data.description); - - if (psa.data.image) embed.setImage(psa.data.image); - (reply.embeds as Writeable).push(embed); - } else if (psa.data.type === "image") { - if (!reply.files) reply.files = []; - (reply.files as Writeable).push( - new AttachmentBuilder(psa.data.image), - ); - reply.content = reply.content ?? psa.data.title; - } - - if (psa.data.links) { - if (!reply.components?.[0]) { - reply.components = [new ActionRowBuilder()]; - } - - ( - reply.components[0] as ActionRowBuilder - ).addComponents( - psa.data.links.map(({ title, url }) => buttonLink(url, title)), - ); - } - } - - if (index === 0) { - await interaction.editReply(reply); - } else { - await interaction.followUp(reply); - } - - if ( - psa.data && - psa.data.secret && - (psa.data.commands === undefined || - psa.data.commands.includes(interaction.commandName)) - ) { - const followUp: InteractionReplyOptions = { - ephemeral: true, - }; - - if (psa.data.type === undefined || psa.data.type === "embed") { - if (!followUp.embeds) followUp.embeds = []; - - const embed = embedWarning(psa.data.title, psa.data.description); - - if (psa.data.image) embed.setImage(psa.data.image); - (followUp.embeds as Writeable).push(embed); - } else if (psa.data.type === "image") { - if (!followUp.files) followUp.files = []; - (followUp.files as Writeable).push( - new AttachmentBuilder(psa.data.image), - ); - followUp.content = followUp.content ?? psa.data.title; - } - - if (psa.data.links) { - if (!followUp.components?.[0]) { - followUp.components = [new ActionRowBuilder()]; - } - - ( - followUp.components[0] as ActionRowBuilder - ).addComponents( - psa.data.links.map(({ title, url }) => buttonLink(url, title)), - ); - } - - interaction.followUp(followUp); - } - - index++; - } - } catch (error) { - const { strings } = translator(interaction.locale); - - const components = [ - new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel(strings.bot.common.errors.get_help) - .setURL("https://discord.gg/nDt7AjGJQH") - .setStyle(ButtonStyle.Link), - ), - ]; - if (error instanceof UserError) { - interaction.editReply({ - content: error.message, - components, - }); - } else { - console.error(interaction.commandName, error); - interaction.editReply({ - content: strings.bot.common.errors.uncaught_error, - components, - }); - } - } -} diff --git a/packages/bot/src/events/interactionCreate/index.ts b/packages/bot/src/events/interactionCreate/index.ts deleted file mode 100644 index 9eef8b024..000000000 --- a/packages/bot/src/events/interactionCreate/index.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { assertSecret } from "@blitzkit/core"; -import { - AttachmentBuilder, - AutocompleteInteraction, - ButtonBuilder, - ButtonInteraction, - type CacheType, - ChatInputCommandInteraction, - EmbedBuilder, - type Interaction, - REST, - Routes, - SlashCommandBuilder, - type SlashCommandOptionsOnlyBuilder, - type SlashCommandSubcommandsOnlyBuilder, -} from "discord.js"; -import { aboutCommand } from "../../commands/about"; -import { breakdownCommand } from "../../commands/breakdown"; -import { debugCommand } from "../../commands/debug"; -import { evolutionCommand } from "../../commands/evolution"; -import { fullStatsCommand } from "../../commands/fullStats"; -import { inactiveCommand } from "../../commands/inactive"; -import { verifyCommand } from "../../commands/link"; -import { ownedTanksCommand } from "../../commands/ownedTanks"; -import { permissionsCommand } from "../../commands/permissions"; -import { pingCommand } from "../../commands/ping"; -import { playerInfoCommand } from "../../commands/playerInfo"; -import { ratingCommand } from "../../commands/rating"; -import { ratingLeaderboardCommand } from "../../commands/ratingLeaderboard"; -import { replayCommand } from "../../commands/replay"; -import { researchCommand } from "../../commands/research"; -import { searchClansCommand } from "../../commands/searchClans"; -import { searchPlayersCommand } from "../../commands/searchPlayers"; -import { statsCommand } from "../../commands/stats"; -import { todayCommand } from "../../commands/today"; -import { RenderConfiguration } from "../../core/blitzkit/renderConfiguration"; -import { handleAutocomplete } from "./handlers/autocomplete"; -import { handleButton } from "./handlers/button"; -import { handleChatInputCommand } from "./handlers/chatInputCommand"; - -export type InteractionRawReturnable = - | string - | EmbedBuilder - | ButtonBuilder - | AttachmentBuilder - | RenderConfiguration - | React.JSX.Element - | null; -export type InteractionIterableReturnable = - | InteractionRawReturnable - | InteractionRawReturnable[]; -export type InteractionReturnable = - | InteractionIterableReturnable - | Promise; - -interface CommandRegistryBase { - command: - | SlashCommandBuilder - | SlashCommandSubcommandsOnlyBuilder - | SlashCommandOptionsOnlyBuilder - | Omit; - - autocomplete?: (interaction: AutocompleteInteraction) => void; - button?: (interaction: ButtonInteraction) => InteractionReturnable; -} - -interface CommandRegistryImplicit extends CommandRegistryBase { - handlesInteraction: true; - handler: (interaction: ChatInputCommandInteraction) => void; -} - -interface CommandRegistryExplicit extends CommandRegistryBase { - handlesInteraction?: false; - handler: ( - interaction: ChatInputCommandInteraction, - ) => InteractionReturnable; -} - -export type CommandRegistry = CommandRegistryImplicit | CommandRegistryExplicit; - -export const commandsArray: Promise[] = [ - // auctionCommand, - permissionsCommand, - debugCommand, - aboutCommand, - inactiveCommand, - ownedTanksCommand, - playerInfoCommand, - searchClansCommand, - searchPlayersCommand, - fullStatsCommand, - breakdownCommand, - verifyCommand, - pingCommand, - evolutionCommand, - statsCommand, - ratingLeaderboardCommand, - todayCommand, - researchCommand, - ratingCommand, - replayCommand, -]; - -export const commands: Record = {}; -const commandsFulfilled: CommandRegistry[] = []; - -let index = 0; -for (const registry of await Promise.allSettled(commandsArray)) { - if (registry.status === "rejected") { - console.warn(`Command ${index} rejected`, registry.reason); - continue; - } - - commandsFulfilled.push(registry.value); - commands[registry.value.command.name] = registry.value; - index++; -} - -const rest = new REST().setToken(assertSecret(import.meta.env.DISCORD_TOKEN)); -const body = commandsFulfilled.map((registry) => registry.command.toJSON()); -const applicationCommandsRoute = Routes.applicationCommands( - assertSecret(import.meta.env.DISCORD_CLIENT_ID), -); - -await rest.put(applicationCommandsRoute, { body }); - -export async function interactionCreate(interaction: Interaction) { - if (interaction.isAutocomplete()) { - handleAutocomplete(interaction); - } else if (interaction.isChatInputCommand()) { - handleChatInputCommand(interaction); - } else if (interaction.isButton()) { - handleButton(interaction); - } -} diff --git a/packages/bot/src/events/ready.ts b/packages/bot/src/events/ready.ts deleted file mode 100644 index 2612b1908..000000000 --- a/packages/bot/src/events/ready.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Client } from 'discord.js'; - -export function ready(client: Client) { - console.log(`🟢 Launched bot ${client.shard?.ids[0] ?? 'default'}`); -} diff --git a/packages/bot/src/main.ts b/packages/bot/src/main.ts deleted file mode 100644 index 8bac09c3a..000000000 --- a/packages/bot/src/main.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { assertSecret } from "@blitzkit/core"; -import { ActivityType, ShardingManager } from "discord.js"; -import { - discoveredIdsDefinitions, - tankDefinitions, -} from "./core/blitzkit/nonBlockingPromises"; - -let manager: ShardingManager; -let iteration = 0; -const interval = setInterval(async () => { - console.log(`Attempting launch on iteration ${++iteration}`); - - manager?.respawnAll(); - manager = new ShardingManager("dist/bot/workers/bot.js", { - token: assertSecret(import.meta.env.DISCORD_TOKEN), - }); - - const shards = await manager - .on("shardCreate", (shard) => { - clearInterval(interval); - console.log(`🟡 Launching shard ${shard.id}`); - shard.on("ready", () => console.log(`🟢 Launched shard ${shard.id}`)); - }) - .spawn({ timeout: 60000 }); - - try { - let servers = 0; - let channels = 0; - let users = 0; - - for (const [, shard] of shards) { - const guilds = (await shard.fetchClientValue("guilds.cache")) as any; - - servers += guilds.length; - - for (const guild of guilds) { - channels += guild.channels.length; - users += guild.memberCount; - } - } - - const awaitedTankDefinitions = await tankDefinitions; - const tanks = Object.values(awaitedTankDefinitions.tanks).length; - const awaitedDiscoveredIdsDefinitions = await discoveredIdsDefinitions; - const usernames = awaitedDiscoveredIdsDefinitions.count; - - manager.broadcastEval( - async (client, { servers, users, channels, tanks, usernames }) => { - const bios = [ - `Living in ${servers.toLocaleString()} servers`, - `Serving ${users.toLocaleString()} users`, - `Observing ${channels.toLocaleString()} channels`, - `Tinkering with ${tanks.toLocaleString()} tanks`, - `Memorizing ${usernames.toLocaleString()} usernames`, - ]; - - client.user?.setPresence({ - activities: [ - { - type: ActivityType.Custom, - name: bios[Math.floor(Math.random() * bios.length)], - }, - ], - }); - - for (const guild of client.guilds.cache.values()) { - try { - const botMember = await guild.members.fetch(client.user.id); - await botMember.setNickname("BlitzKit"); - } catch {} - } - }, - { context: { servers, channels, users, tanks, usernames } }, - ); - } catch (error) { - console.warn( - "An error occurred when setting up presence, ignoring to keep the process alive", - error, - ); - } -}, 15000); diff --git a/packages/bot/src/stitches.config.ts b/packages/bot/src/stitches.config.ts deleted file mode 100644 index f50f1a5fa..000000000 --- a/packages/bot/src/stitches.config.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { - amberDark, - amberDarkA, - blueDark, - brownDark, - crimsonDark, - greenDark, - mauveDark, - orangeDark, - purpleDark, - redDark, - rubyDark, - sandDark, - slate, - slateDark, - slateDarkA, - tealDark, -} from '@radix-ui/colors'; -import { createBorderStyles, createColors, createSpaces } from 'bepaint'; - -// this isn't really a stitches config, but the object is compliant -export const theme = { - spaces: createSpaces(), - - colors: { - ...createColors(mauveDark), - ...createColors(slateDarkA, 'alpha'), - ...createColors(slate, 'light'), - - ...createColors(amberDark, 'amber'), - ...createColors(amberDarkA, 'amberAlpha'), - ...createColors(orangeDark, 'orange'), - ...createColors(purpleDark, 'purple'), - ...createColors(blueDark, 'blue'), - ...createColors(tealDark, 'teal'), - ...createColors(greenDark, 'green'), - ...createColors(redDark, 'red'), - ...createColors(brownDark, 'brown'), - ...createColors(mauveDark, 'mauve'), - ...createColors(sandDark, 'sand'), - ...createColors(crimsonDark, 'crimson'), - ...createColors(rubyDark, 'ruby'), - }, - - borderStyles: { - subtle: `1px solid ${slateDark.slate2}`, - ...createBorderStyles(slateDark), - ...createBorderStyles(blueDark, undefined, 'blue'), - }, - - durations: { - regular: '0.2s', - }, -}; diff --git a/packages/bot/src/types.d.ts b/packages/bot/src/types.d.ts deleted file mode 100644 index 29102695c..000000000 --- a/packages/bot/src/types.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.ttf' { - const path: string; - export default path; -} diff --git a/packages/bot/src/types/histories.ts b/packages/bot/src/types/histories.ts deleted file mode 100644 index 297552339..000000000 --- a/packages/bot/src/types/histories.ts +++ /dev/null @@ -1,24 +0,0 @@ -// a cleaner simplification of Blitz Stars' histories because it's a mess there - -import { type BlitzStats } from '@blitzkit/core'; - -export interface History { - all: BlitzStats; - last_battle_time: number; -} - -export type Histories = History[]; - -export interface GetHistoriesOptions { - start: number; - end: number; - includeLatestHistories: boolean; - includePreviousHistories: boolean; -} - -export const getHistoriesDefaultOptions: GetHistoriesOptions = { - start: -Infinity, - end: Infinity, - includeLatestHistories: false, - includePreviousHistories: false, -}; diff --git a/packages/bot/src/types/possiblyPromise.ts b/packages/bot/src/types/possiblyPromise.ts deleted file mode 100644 index a4658c873..000000000 --- a/packages/bot/src/types/possiblyPromise.ts +++ /dev/null @@ -1 +0,0 @@ -export type PossiblyPromise = Promise | Type; diff --git a/packages/bot/src/types/ttf.d.ts b/packages/bot/src/types/ttf.d.ts deleted file mode 100644 index 29102695c..000000000 --- a/packages/bot/src/types/ttf.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.ttf' { - const path: string; - export default path; -} diff --git a/packages/bot/src/types/writable.ts b/packages/bot/src/types/writable.ts deleted file mode 100644 index 2f45b1129..000000000 --- a/packages/bot/src/types/writable.ts +++ /dev/null @@ -1 +0,0 @@ -export type Writeable = { -readonly [P in keyof T]: T[P] }; diff --git a/packages/bot/src/workers/bot.ts b/packages/bot/src/workers/bot.ts deleted file mode 100644 index b3af429ca..000000000 --- a/packages/bot/src/workers/bot.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { assertSecret } from "@blitzkit/core"; -import { registerProcesses } from "../core/blitzkit/processes"; -import { client } from "../core/discord/client"; - -console.log(`🟡 Launching bot ${client.shard?.ids[0]}`); - -registerProcesses(); - -console.log(`post register processes on shard ${client.shard?.ids[0]}`); - -client.login(assertSecret(import.meta.env.DISCORD_TOKEN)); - -console.log(`post client login on shard ${client.shard?.ids[0]}`); diff --git a/packages/bot/src/workers/render.ts b/packages/bot/src/workers/render.ts deleted file mode 100644 index 1838e44ad..000000000 --- a/packages/bot/src/workers/render.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { parentPort, threadId } from 'worker_threads'; -import { svgToPng } from '../core/blitzkit/svgToPng'; - -console.log(`🟢 Launched render worker ${threadId}`); - -parentPort?.on('message', async (svg: string) => { - const png = svgToPng(svg); - parentPort?.postMessage(png); -}); - -console.log(`post render thread parent port on thread ${threadId}`); diff --git a/packages/bot/tsconfig.json b/packages/bot/tsconfig.json deleted file mode 100644 index d24722b0d..000000000 --- a/packages/bot/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "include": ["src/**/*", "../core/src/blitzkit/supportedLocales.ts", "../i18n/src/unwrapper.ts"] -} diff --git a/packages/i18n/strings/en.json b/packages/i18n/strings/en.json index 8e5746268..418c90783 100644 --- a/packages/i18n/strings/en.json +++ b/packages/i18n/strings/en.json @@ -204,536 +204,6 @@ } }, - "bot": { - "common": { - "errors": { - "clan_not_found": "# Could not find clan\nCouldn't find \"{search}\". Try selecting a clan from the search result.", - "player_not_found": "# Could not find user\nI couldn't find user \"{search}\". Try picking an user from the search result, typing in a valid username, or using the `/link` command.", - "player_not_linked": "# Use `/link` to get started\nI don't know who you are but you can tell me today!", - "tank_name_not_found": "# Tank not found\nCould not find tank by the name \"{search}\".", - "tank_id_not_found": "# Tank not found\nCould not find tank by the ID \"{search}\". Try typing the name of the tank instead of a number.", - "no_tank_stats": "# No tank stats available\nThis player doesn't have any stats available. This may not be the player you are looking for.", - "get_help": "Help Discord server", - "uncaught_error": "# BlitzKit ran into an error!\nWe're so sorry about this. Feel free to [join the official Discord server](https://discord.gg/nDt7AjGJQH) to get help.", - "preview": "# This command is in preview\n\nYou can only access this command in [the official Discord server](https://discord.gg/nDt7AjGJQH). The command will be generally available soon.", - "query_too_small": "# Your search is too short\nYour search must be at least 3 characters long.", - "query_too_long": "# Your search is too long\nYour search must be at most 100 characters long." - }, - - "wn8": { - "name": "WN8", - - "types": { - "very_bad": "Very bad", - "bad": "Bad", - "below_average": "Below average", - "average": "Average", - "above_average": "Above average", - "good": "Good", - "very_good": "Very good", - "great": "Great", - "unicum": "Unicum", - "super_unicum": "Super unicum" - } - }, - - "subcommands": { - "today": { - "name": "today", - "description": "Today" - }, - "30": { - "name": "30", - "description": "30 days" - }, - "60": { - "name": "60", - "description": "60 days" - }, - "90": { - "name": "90", - "description": "90 days" - }, - "career": { - "name": "career", - "description": "Career" - }, - "custom": { - "name": "custom", - "description": "Custom range", - - "options": { - "start": { - "name": "start", - "description": "Start in number of days ago from today" - }, - "end": { - "name": "end", - "description": "End in number of days ago from today" - } - } - } - }, - - "options": { - "clan": { - "name": "clan", - "description": "The clan name or tag" - }, - - "tank": { - "name": "tank", - "description": "The tank to get stats for" - }, - - "username": { - "name": "username", - "description": "The Blitz username of the player" - }, - - "filters": { - "nation": { - "name": "nation", - "description": "Nation" - }, - "class": { - "name": "class", - "description": "Class" - }, - "type": { - "name": "type", - "description": "Type" - } - }, - - "region": { - "name": "region", - "description": "Region" - }, - - "tier": { - "name": "tier", - "description": "Tier", - - "choices": { - "tier": "Tier {tier_1} - {tier_2}" - } - } - }, - - "periods": { - "career": "Career", - "today": "Today", - "days": "{days} days", - "custom": "{day_1} - {day_2} days" - }, - - "filters": { - "none": "No filters", - "tier": "Tier {tier}" - }, - - "no_data": { - "battles_in_period": "No battles played in this period", - "battles_in_season": "No battles played in this season", - "players_in_period": "No players in this period", - "tanks_found": "No tanks found" - }, - - "buttons": { - "refresh": "Refresh" - }, - - "hero_stats": { - "battles": "Battles", - "winrate": "Winrate", - "damage": "Damage", - "damage_ratio": "Damage ratio", - "tier": "Tier", - "survival": "Survival", - "accuracy": "Accuracy", - "kills": "Kills" - } - }, - - "commands": { - "dead": "# Stat Commands Are Dead\n\nThe BlitzKit Discord bot used to use BlitzStars to fetch periodic statistics. BlitzStars has enabled aggressive Cloudflare protections; hence, disabling BlitzKit from using BlitzStar's API. This has impacted the following commands:\n\n- `/stats`\n- `/today`\n- `/breakdown`\n- `/evolution`\n- `/full-stats`\n\nPlease use [Aftermath]() as an alternative which is completely independent of BlitzStars. BlitzKit-hosted stat commands will return in the near future.", - - "replay": { - "name": "replay", - "description": "Display replay results", - - "options": { - "file": { - "name": "file", - "description": "Replay file" - } - } - }, - - "research": { - "name": "research", - "description": "Lists the expenses in researching a line", - - "options": { - "target_tank": { - "name": "target-tank", - "description": "The final tank to research in the line" - }, - "starting_tank": { - "name": "starting-tank", - "description": "The starting tank in the line (inferred automatically if not provided)" - } - }, - - "body": { - "subtitle": "Research", - "total": "Total", - "research": "Research: {cost}", - "upgrades": "Upgrades: {cost}", - "purchase": "Purchase: {cost}", - "equipment": "Equipment: {cost}", - "estimation": "# This is an estimation!\n\nThis list may include already researched tanks because of the inaccurate information Wargaming provides. Try providing a `starting-tank` from the line when using the command if you can." - }, - - "errors": { - "non_tech_tree": "# {tank} is not on the tech tree\n\nYou cannot research a premium/collector tank.", - "tier_1": "# {tank} is a tier I\n\nTier I tanks are always unlocked for free for all players.", - "already_researched": "# {tank} is already researched\n\nTry providing a `starting-tank` when using the command to view the totals for a line you have already researched.", - "start_end_equal": "# {tank} was provided twice\n\nMake sure `starting-tank` and `target-tank` are different.", - "tanks_not_on_line": "# {tank_1} and {tank_2} are not on the same line\n\nMake sure `starting-tank` and `target-tank` are on the same tech-tree line.", - "unordered": "# {tank_1} higher tiered to {tank_2}\n\nMake sure to provide {tank_2} as the `starting-tank` instead as it is the lower tiered tank." - } - }, - - "about": { - "name": "about", - "description": "All the info you need about the bot", - "subcommands": { - "commands": { - "name": "commands", - "description": "List of all commands and their descriptions", - "body": "# Commands\n\nBlitzKit offers the following commands:\n\n" - }, - "embeds": { - "name": "embeds", - "description": "How embeds work" - }, - "introduction": { - "name": "introduction", - "description": "About the bot" - }, - "invite": { - "name": "invite", - "description": "How to invite the bot" - }, - "timezones": { - "name": "timezones", - "description": "How BlitzKit infers and uses your timezone" - }, - "credits": { - "name": "credits", - "description": "People who have helped BlitzKit" - } - } - }, - - "breakdown": { - "name": "breakdown", - "description": "A period's breakdown by tanks played", - - "body": { - "total": "Total", - "battles": "Battles", - "wn8": "WN8", - "winrate": "Winrate", - "damage": "Damage", - "unknown_tank": "Unknown tank {id}" - } - }, - - "debug": { - "name": "debug", - "description": "Debug information about the bot", - - "body": { - "title": "# Debug\n\n", - "version": "Version", - "shard": { - "label": "Shard", - "default": "default" - }, - "uptime": "Uptime" - } - }, - - "evolution": { - "name": "evolution", - "description": "Evolution of statistics", - - "groups": { - "player": { - "name": "player", - "description": "Player statistics" - }, - - "tank": { - "name": "tank", - "description": "Tank statistics" - } - } - }, - - "inactive": { - "name": "inactive", - "description": "Lists all inactive players in a clan above a threshold", - - "options": { - "threshold": { - "name": "threshold", - "description": "The number of days inactive (default: {days} days)" - } - }, - - "body": { - "subtitle": "Inactive for {days}+ days", - "listing": "{days} days" - } - }, - - "full_stats": { - "name": "full-stats", - "description": "Full in-game statistics", - - "body": { - "cards": { - "games": { - "title": "Games", - "winrate": "Winrate", - "battles": "Battles", - "wins_losses": "Wins / losses" - }, - "efficiency": { - "title": "Efficiency", - "wn8": "WN8", - "survival": "Survival", - "accuracy": "Accuracy" - }, - "lethality": { - "title": "Lethality", - "average_damage": "Average damage", - "damage_per_tier": "Damage per tier", - "damage_ratio": "Damage ratio", - "average_kills": "Average kills" - }, - "miscellaneous": { - "title": "Miscellaneous", - "average_tier": "Average tier", - "average_spots": "Average spots", - "average_xp": "Average XP", - "kills_to_death_ratio": "Kills to death ratio" - } - } - } - }, - - "link": { - "name": "link", - "description": "Links your Blitz and Discord account", - - "body": { - "accounts_linked": "Accounts linked", - "no_role_permissions": { - "title": "{name} failed to verify", - "description": "I don't have the permission to manage roles." - } - }, - - "embed": { - "title": "Incorrect account?", - "description": "Use the command again and select a username from the search results." - } - }, - - "search_clans": { - "name": "search-clans", - "description": "Search clans", - - "options": { - "limit": { - "name": "limit", - "description": "The max number of clan results (default: {limit})" - } - }, - - "body": { - "title": "Clan search for {search} in region {region}", - "no_results": "No clans found" - } - }, - - "search_players": { - "name": "search-players", - "description": "Search players", - - "options": { - "limit": { - "name": "limit", - "description": "The max number of player results (default: {limit})" - } - }, - - "body": { - "title": "Player search for {search} in region {region}", - "no_results": "No players found" - } - }, - - "rating_leaderboard": { - "name": "rating-leaderboard", - "description": "Rating battles statistics", - - "subcommands": { - "neighbors": { - "name": "neighbors", - "description": "Neighboring players on the leaderboard" - }, - - "league": { - "name": "league", - "description": "Top players of a league" - } - }, - - "options": { - "limit": { - "name": "limit", - "description": "How many neighbors to display (default: {count})" - }, - - "season": { - "name": "season", - "description": "Season number (default: current)", - - "choices": { - "current": "{season} (current)" - } - }, - - "league": { - "name": "league", - "description": "The league to display" - } - }, - - "body": { - "top_players": "Top {count} players", - "no_participation": "# {player} didn't play rating in season {season}\n\nThis player did not participate in this season or did not get past calibration.", - "no_ongoing_season": "# No ongoing season\n\nThere isn't a currently running rating season.", - "deleted_player": "Deleted {id}", - "neighbors": "Rating neighbors", - "more": "+ {count} more" - } - }, - - "today": { - "name": "today", - "description": "Today's played tanks and statistics" - }, - - "stats": { - "name": "stats", - "description": "Quick statistics for a period" - }, - - "owned_tanks": { - "name": "owned-tanks", - "description": "Shows all own tanks in a tier", - - "body": { - "title": "# {name}'s owned tier {tier} tanks\n\n" - } - }, - - "permissions": { - "name": "permissions", - "description": "Checks wether the bot has the required permissions to function", - - "body": { - "title": "# Permissions\n\n{body}", - "view_channels": "View channels: needed to find the message the refresh button was used on", - "read_message_history": "Read message history: needed to find the message to refresh", - "attach_files": "Attach files: needed to add images to image-based commands", - "external_emojis": "External emojis: needed to add national flags and tank class emojis" - } - }, - - "ping": { - "name": "ping", - "description": "Check wether the bot is alive.", - - "body": { - "pong": "Pong 🏓", - "pong_with_time": "Pong 🏓 - {time}ms" - }, - - "subcommands": { - "blitzkit": { - "name": "blitzkit", - "description": "Ping BlitzKit" - }, - "wotb": { - "name": "wotb", - "description": "Ping World of Tanks Blitz" - } - } - }, - - "player_info": { - "name": "player-info", - "description": "Basic information about a player", - - "body": { - "title": "{name}'s information", - "nickname": "Nickname", - "battles": "Battles", - "winrate": "Winrate", - "account_id": "Account ID", - "created": "Created", - "last_battle": "Last battle" - } - }, - - "rating": { - "name": "rating", - "description": "Rating player's statistics", - - "subcommands": { - "today": { - "name": "today", - "description": "Today's rating statistics" - }, - "season": { - "name": "season", - "description": "This season's rating statistics" - } - }, - - "body": { - "subtitle": { - "today": "Today's rating statistics", - "season": "This season's rating statistics" - } - }, - - "errors": { - "no_participation": "# {name} hasn't played rating\n\nThis player hasn't participated in the rating gamemode this season.", - "unrecorded_stats": "# Statistics currently unavailable\n\nWargaming may be currently be facing server downtime. If there is an on-going season, statistics should be available withing 24 hours." - } - } - } - }, - "website": { "redirect": { "title": "301: moved permanently", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 8e788dc2d..41678e3ad 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -41,7 +41,6 @@ "setup:patches": "bun src/applyPatches.ts", "build:reviews": "bun src/reviews.ts", "build:ids": "bun src/discoverIds.ts", - "build:bot": "bun src/buildBot.ts", "build:ratingleaderboard": "bun src/buildRatingLeaderboard.ts", "build:proxyclient": "bun src/buildProxyClient.ts", "build:catalogitemaccesssor": "bun src/buildCatalogItemAccessor.ts", diff --git a/packages/scripts/src/buildBot.ts b/packages/scripts/src/buildBot.ts deleted file mode 100644 index 379bfc72a..000000000 --- a/packages/scripts/src/buildBot.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { build } from "esbuild"; -import { cp, readdir, rm } from "fs/promises"; - -const projectDir = "../.."; -const srcRoot = `${projectDir}/packages/bot`; -const distRoot = `${projectDir}/dist/bot`; -const modulesRoot = "../../node_modules"; -const workers = await readdir(`${srcRoot}/src/workers`); - -await rm(distRoot, { recursive: true, force: true }); - -build({ - entryPoints: [ - `${srcRoot}/src/main.ts`, - ...workers.map((worker) => `${srcRoot}/src/workers/${worker}`), - ], - bundle: true, - outdir: "../../dist/bot", - platform: "node", - format: "esm", - loader: { - ".ttf": "file", - ".node": "empty", - ".json": "json", - }, - external: ["sharp"], - minifyIdentifiers: false, // causes goofy ahh issues - minifySyntax: true, - minifyWhitespace: true, - sourcemap: true, - jsxImportSource: "react", - jsx: "automatic", -}); - -for (const module of await readdir(`${modulesRoot}/@resvg`)) { - readdir(`${modulesRoot}/@resvg/${module}`).then((files) => - files - .filter((file) => file.endsWith(".node")) - .map((file) => - cp( - `${modulesRoot}/@resvg/${module}/${file}`, - `${distRoot}/workers/${file}`, - ), - ), - ); -} - -await cp(`${projectDir}/prisma`, `${distRoot}/prisma`, { recursive: true }); - -for (const file of await readdir(`${modulesRoot}/prisma`).then((files) => - files.filter((file) => file.endsWith(".node")), -)) { - cp(`${modulesRoot}/prisma/${file}`, `${distRoot}/prisma/${file}`); -} diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index dcc620ae3..000000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,24 +0,0 @@ -generator client { - provider = "prisma-client-js" - binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] - output = "../packages/bot/src/prisma" -} - -datasource db { - provider = "cockroachdb" - url = env("DATABASE_URL") -} - -model User { - blitz_id BigInt @id @map("blitz") // map: "blitz" was too vague - last_used DateTime? @default(now()) - - @@map("Users") // map: "Users" was mistakenly plural -} - -model DiscordUser { - discord_id BigInt @id @map("discord") // map: "discord" was too vague - blitz_id BigInt @map("blitz") // map: "blitz" was too vague - - @@map("DiscordUsers") // map: "DiscordUsers" was mistakenly plural -}