mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-25 03:29:51 +01:00
fix: types
This commit is contained in:
1 parent
9ae8b0ac49
commit
3c80a43435
255 files changed
+797
-838
No files matched your search
@@ -0,0 +1,13 @@
|
||||
import { idToRegion, usersDatabase } from '@blitzkit/core';
|
||||
import { flagUserActivity } from '../../../../website/src/core/blitzkit/flagUserActivity';
|
||||
|
||||
export async function getBlitzFromDiscord(discordId: bigint) {
|
||||
const unique = await usersDatabase.discordUser.findUnique({
|
||||
where: { discord_id: discordId },
|
||||
});
|
||||
|
||||
if (unique === null) return null;
|
||||
|
||||
await flagUserActivity(unique.blitz_id);
|
||||
return { id: unique.blitz_id, region: idToRegion(unique.blitz_id) };
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function getPeriodNow() {
|
||||
return Date.now() / 1000;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Region } from '@blitzkit/core';
|
||||
import getTimeDaysAgo from '../../../../website/src/core/blitzkit/getTimeDaysAgo';
|
||||
import { PeriodSize } from '../discord/addPeriodSubCommands';
|
||||
|
||||
export default function getPeriodStart(region: Region, period: PeriodSize) {
|
||||
if (period === 'career') return 0;
|
||||
if (period === 'today') return getTimeDaysAgo(region, 1);
|
||||
return getTimeDaysAgo(region, parseInt(period));
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import jsxToSvg from './jsxToSvg';
|
||||
import svgToPng from './svgToPng';
|
||||
|
||||
export default async function jsxToPng(jsx: JSX.Element) {
|
||||
const svg = await jsxToSvg(jsx);
|
||||
const png = svgToPng(svg);
|
||||
|
||||
return png;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { RenderConfiguration } from '../../../../website/src/core/blitzkit/renderConfiguration';
|
||||
import jsxToSvg from './jsxToSvg';
|
||||
import { svgToPngThreaded } from './svgToPngThreaded';
|
||||
|
||||
export default async function jsxToPngThreaded(
|
||||
jsx: JSX.Element,
|
||||
renderConfiguration: RenderConfiguration,
|
||||
) {
|
||||
const svg = await jsxToSvg(jsx, renderConfiguration);
|
||||
const png = await svgToPngThreaded(svg);
|
||||
return png;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,23 @@
|
||||
import { readFile } from 'fs/promises';
|
||||
import satori from 'satori';
|
||||
import { RenderConfiguration } from '../../../../../website/src/core/blitzkit/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(`dist/bot/workers/${file}`);
|
||||
return { name: 'Roboto', weight, data };
|
||||
});
|
||||
|
||||
export default async function jsxToSvg(
|
||||
jsx: JSX.Element,
|
||||
renderConfiguration = new RenderConfiguration(),
|
||||
) {
|
||||
const fonts = await Promise.all(fontsPromise);
|
||||
return await satori(jsx, { width: renderConfiguration.width, fonts });
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Resvg } from '@resvg/resvg-js';
|
||||
|
||||
export default function svgToPng(svg: string) {
|
||||
return new Resvg(svg).render().asPng();
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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<Buffer>((resolve) => {
|
||||
queue.push({ svg, resolve });
|
||||
manageQueue();
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { BlitzStats, emptyAllStats, Region } from '@blitzkit/core';
|
||||
import { Locale } from 'discord.js';
|
||||
import { Region } from '../../../../../src/constants/regions';
|
||||
import getTankHistories, {
|
||||
TankHistories,
|
||||
TankHistoryRaw,
|
||||
} from '../../../../../src/core/blitzkit/getTankHistories';
|
||||
import { BlitzStats } from '../../../../../src/core/statistics/compositeStats/constants';
|
||||
import { emptyAllStats } from './constants';
|
||||
} from './getTankHistories';
|
||||
|
||||
const emptyTankHistoryNode: TankHistoryRaw = {
|
||||
account_id: 0,
|
||||
@@ -106,5 +104,3 @@ export default async function getStatsInPeriod(
|
||||
|
||||
return { diff, order } satisfies DiffedTankStats;
|
||||
}
|
||||
|
||||
export * from './constants';
|
||||
@@ -0,0 +1,123 @@
|
||||
import { Region, emptyAllStats } from '@blitzkit/core';
|
||||
import { Locale } from 'discord.js';
|
||||
import getTankStats from '../../../../website/src/core/blitz/getTankStats';
|
||||
import { UserError } from '../../../../website/src/hooks/userError';
|
||||
import {
|
||||
GetHistoriesOptions,
|
||||
Histories,
|
||||
History,
|
||||
getHistoriesDefaultOptions,
|
||||
} from '../../types/histories';
|
||||
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 default async function getTankHistories(
|
||||
server: Region,
|
||||
id: number,
|
||||
locale: Locale,
|
||||
options?: Partial<GetTankHistoriesOptions>,
|
||||
) {
|
||||
const { t } = 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<number, number> = {};
|
||||
const lastNonCompliantIndex: Record<number, number> = {};
|
||||
const hasCompliedOnce: Record<number, boolean> = {};
|
||||
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(t`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;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Locale, SlashCommandStringOption } from 'discord.js';
|
||||
import { translator } from '../localization/translator';
|
||||
import { localizationObject } from './localizationObject';
|
||||
|
||||
export default function addClanChoices(option: SlashCommandStringOption) {
|
||||
const { translate } = translator(Locale.EnglishUS);
|
||||
|
||||
return option
|
||||
.setName('clan')
|
||||
.setNameLocalizations(localizationObject('bot.common.options.clan'))
|
||||
.setDescription(translate('bot.common.options.clan.description'))
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject('bot.common.options.clan.description'),
|
||||
)
|
||||
.setAutocomplete(true)
|
||||
.setMinLength(2)
|
||||
.setRequired(true);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { TANK_CLASSES, TREE_TYPES } from '@blitzkit/core';
|
||||
import {
|
||||
APIApplicationCommandOptionChoice,
|
||||
Locale,
|
||||
SlashCommandBuilder,
|
||||
SlashCommandSubcommandBuilder,
|
||||
} from 'discord.js';
|
||||
import {} from '../../components/Tanks/components/Item/constants';
|
||||
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 { translate } = translator(Locale.EnglishUS);
|
||||
|
||||
return (
|
||||
option
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName(translate('bot.common.options.filters.nation'))
|
||||
.setNameLocalizations(
|
||||
localizationObject('bot.common.options.filters.nation'),
|
||||
)
|
||||
.setDescription(
|
||||
translate('bot.common.options.filters.nation.description'),
|
||||
)
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject('bot.common.options.filters.nation.description'),
|
||||
)
|
||||
.addChoices(
|
||||
...nations.map(
|
||||
(nation) =>
|
||||
({
|
||||
value: nation,
|
||||
name: translate(`common.nations.${nation}`),
|
||||
name_localizations: localizationObject(
|
||||
`common.nations.${nation}`,
|
||||
),
|
||||
} satisfies APIApplicationCommandOptionChoice),
|
||||
),
|
||||
)
|
||||
.setRequired(false),
|
||||
)
|
||||
.addStringOption((option) => addTierChoices(option).setRequired(false))
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName(translate('bot.common.options.filters.tank_class'))
|
||||
.setNameLocalizations(
|
||||
localizationObject('bot.common.options.filters.tank_class'),
|
||||
)
|
||||
.setDescription(
|
||||
translate('bot.common.options.filters.tank_class.description'),
|
||||
)
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject(
|
||||
'bot.common.options.filters.tank_class.description',
|
||||
),
|
||||
)
|
||||
.addChoices(
|
||||
...TANK_CLASSES.map(
|
||||
(tankType) =>
|
||||
({
|
||||
value: tankType,
|
||||
name: translate(`common.tank_class_short.${tankType}`),
|
||||
name_localizations: localizationObject(
|
||||
`common.tank_class_short.${tankType}`,
|
||||
),
|
||||
} satisfies APIApplicationCommandOptionChoice),
|
||||
),
|
||||
)
|
||||
.setRequired(false),
|
||||
)
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName(translate('bot.common.options.filters.tree_type'))
|
||||
.setNameLocalizations(
|
||||
localizationObject('bot.common.options.filters.tree_type'),
|
||||
)
|
||||
.setDescription(
|
||||
translate('bot.common.options.filters.tree_type.description'),
|
||||
)
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject(
|
||||
'bot.common.options.filters.tree_type.description',
|
||||
),
|
||||
)
|
||||
.addChoices(
|
||||
...TREE_TYPES.map(
|
||||
(treeType) =>
|
||||
({
|
||||
value: treeType,
|
||||
name: translate(`common.tree_type.${treeType}`),
|
||||
name_localizations: localizationObject(
|
||||
`common.tree_type.${treeType}`,
|
||||
),
|
||||
} satisfies APIApplicationCommandOptionChoice),
|
||||
),
|
||||
)
|
||||
.setRequired(false),
|
||||
)
|
||||
// TODO: somehow localize this?
|
||||
.addStringOption((option) =>
|
||||
addTankChoices(option).setRequired(false),
|
||||
) as Option
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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 { translate } = translator(Locale.EnglishUS);
|
||||
const subcommandNameLocalizations = {
|
||||
today: localizationObject('bot.common.subcommands.today'),
|
||||
30: localizationObject('bot.common.subcommands.30'),
|
||||
60: localizationObject('bot.common.subcommands.60'),
|
||||
90: localizationObject('bot.common.subcommands.90'),
|
||||
career: localizationObject('bot.common.subcommands.career'),
|
||||
custom: localizationObject('bot.common.subcommands.custom'),
|
||||
};
|
||||
const subcommandDescriptionLocalizations = {
|
||||
today: localizationObject('bot.common.subcommands.today.description'),
|
||||
30: localizationObject('bot.common.subcommands.30.description'),
|
||||
60: localizationObject('bot.common.subcommands.60.description'),
|
||||
90: localizationObject('bot.common.subcommands.90.description'),
|
||||
career: localizationObject('bot.common.subcommands.career.description'),
|
||||
custom: localizationObject('bot.common.subcommands.custom.description'),
|
||||
};
|
||||
|
||||
option
|
||||
.addSubcommand((option) =>
|
||||
extra(
|
||||
option
|
||||
.setName(translate('bot.common.subcommands.today'))
|
||||
.setNameLocalizations(subcommandNameLocalizations.today)
|
||||
.setDescription(translate('bot.common.subcommands.today.description'))
|
||||
.setDescriptionLocalizations(
|
||||
subcommandDescriptionLocalizations.today,
|
||||
),
|
||||
),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(
|
||||
option
|
||||
.setName(translate('bot.common.subcommands.30'))
|
||||
.setNameLocalizations(subcommandNameLocalizations[30])
|
||||
.setDescription(translate('bot.common.subcommands.30.description'))
|
||||
.setDescriptionLocalizations(subcommandDescriptionLocalizations[30]),
|
||||
),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(
|
||||
option
|
||||
.setName(translate('bot.common.subcommands.60'))
|
||||
.setNameLocalizations(subcommandNameLocalizations[60])
|
||||
.setDescription(translate('bot.common.subcommands.60.description'))
|
||||
.setDescriptionLocalizations(subcommandDescriptionLocalizations[60]),
|
||||
),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(
|
||||
option
|
||||
.setName(translate('bot.common.subcommands.90'))
|
||||
.setNameLocalizations(subcommandNameLocalizations[90])
|
||||
.setDescription(translate('bot.common.subcommands.90.description'))
|
||||
.setDescriptionLocalizations(subcommandDescriptionLocalizations[90]),
|
||||
),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(
|
||||
option
|
||||
.setName(translate('bot.common.subcommands.career'))
|
||||
.setNameLocalizations(subcommandNameLocalizations.career)
|
||||
.setDescription(
|
||||
translate('bot.common.subcommands.career.description'),
|
||||
)
|
||||
.setDescriptionLocalizations(
|
||||
subcommandDescriptionLocalizations.career,
|
||||
),
|
||||
),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(
|
||||
option
|
||||
.setName(translate('bot.common.subcommands.custom'))
|
||||
.setNameLocalizations(subcommandNameLocalizations.custom)
|
||||
.setDescription(
|
||||
translate('bot.common.subcommands.custom.description'),
|
||||
)
|
||||
.setDescriptionLocalizations(
|
||||
subcommandDescriptionLocalizations.custom,
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName(translate('bot.common.subcommands.custom.options.start'))
|
||||
.setDescription(
|
||||
translate(
|
||||
'bot.common.subcommands.custom.options.start.description',
|
||||
),
|
||||
)
|
||||
.setRequired(true),
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName(translate('bot.common.subcommands.custom.options.end'))
|
||||
.setDescription(
|
||||
translate(
|
||||
'bot.common.subcommands.custom.options.end.description',
|
||||
),
|
||||
)
|
||||
.setRequired(true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { gameDefinitions } from '@blitzkit/core/src/blitzkit/gameDefinitions';
|
||||
import {
|
||||
SlashCommandBuilder,
|
||||
SlashCommandSubcommandBuilder,
|
||||
SlashCommandSubcommandGroupBuilder,
|
||||
} from 'discord.js';
|
||||
import { addFilterOptions } from './addFilterOptions';
|
||||
import { addPeriodSubCommands } from './addPeriodSubCommands';
|
||||
|
||||
export default async function addPeriodicFilterOptions<
|
||||
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
|
||||
>(
|
||||
option: OptionType,
|
||||
extra: (
|
||||
option: SlashCommandSubcommandBuilder,
|
||||
) => SlashCommandSubcommandBuilder = (option) => option,
|
||||
): Promise<OptionType> {
|
||||
const awaitedGameDefinitions = await gameDefinitions;
|
||||
|
||||
addPeriodSubCommands(option, (option) =>
|
||||
addFilterOptions(extra(option), awaitedGameDefinitions.nations),
|
||||
);
|
||||
|
||||
return option;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { REGIONS } from '@blitzkit/core';
|
||||
import {
|
||||
APIApplicationCommandOptionChoice,
|
||||
Locale,
|
||||
SlashCommandStringOption,
|
||||
} from 'discord.js';
|
||||
import { translator } from '../localization/translator';
|
||||
import { localizationObject } from './localizationObject';
|
||||
|
||||
export default function addRegionChoices(option: SlashCommandStringOption) {
|
||||
const { t, translate } = translator(Locale.EnglishUS);
|
||||
|
||||
return option
|
||||
.setName(t`bot.common.options.region`)
|
||||
.setNameLocalizations(localizationObject('bot.common.options.region'))
|
||||
.setDescription(t`bot.common.options.region.description`)
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject('bot.common.options.region.description'),
|
||||
)
|
||||
.addChoices(
|
||||
...REGIONS.map(
|
||||
(region) =>
|
||||
({
|
||||
value: region,
|
||||
name: translate(`common.regions.normal.${region}`),
|
||||
name_localizations: localizationObject(
|
||||
`common.regions.normal.${region}`,
|
||||
),
|
||||
} satisfies APIApplicationCommandOptionChoice),
|
||||
),
|
||||
)
|
||||
.setRequired(true);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Locale, SlashCommandStringOption } from 'discord.js';
|
||||
import { translator } from '../localization/translator';
|
||||
import { localizationObject } from './localizationObject';
|
||||
|
||||
export default function addTankChoices(option: SlashCommandStringOption) {
|
||||
const { translate } = translator(Locale.EnglishUS);
|
||||
|
||||
return option
|
||||
.setName(translate('bot.common.options.tank'))
|
||||
.setNameLocalizations(localizationObject('bot.common.options.tank'))
|
||||
.setDescription(translate('bot.common.options.tank.description'))
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject('bot.common.options.tank.description'),
|
||||
)
|
||||
.setAutocomplete(true)
|
||||
.setRequired(true);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import numberToRomanNumeral from '@blitzkit/core/src/math/numberToRomanNumeral';
|
||||
import {
|
||||
APIApplicationCommandOptionChoice,
|
||||
Locale,
|
||||
SlashCommandStringOption,
|
||||
} from 'discord.js';
|
||||
import { range } from 'lodash';
|
||||
import { translator } from '../localization/translator';
|
||||
import { localizationObject } from './localizationObject';
|
||||
|
||||
export default function addTierChoices(option: SlashCommandStringOption) {
|
||||
const { t, translate } = translator(Locale.EnglishUS);
|
||||
|
||||
return option
|
||||
.setName(t`bot.common.options.tier`)
|
||||
.setNameLocalizations(localizationObject('bot.common.options.tier'))
|
||||
.setDescription(t`bot.common.options.tier.description`)
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject('bot.common.options.tier.description'),
|
||||
)
|
||||
.setChoices(
|
||||
...range(10, 0).map(
|
||||
(tier) =>
|
||||
({
|
||||
name: translate('bot.common.options.tier.choices.tier', [
|
||||
`${tier}`,
|
||||
numberToRomanNumeral(tier),
|
||||
]),
|
||||
value: `${tier}`,
|
||||
name_localizations: localizationObject(
|
||||
'bot.common.options.tier.choices.tier',
|
||||
[`${tier}`, numberToRomanNumeral(tier)],
|
||||
),
|
||||
} satisfies APIApplicationCommandOptionChoice),
|
||||
),
|
||||
)
|
||||
.setRequired(true);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Locale, SlashCommandStringOption } from 'discord.js';
|
||||
import { translator } from '../localization/translator';
|
||||
import { localizationObject } from './localizationObject';
|
||||
|
||||
export default function addUsernameChoices(option: SlashCommandStringOption) {
|
||||
const { t } = translator(Locale.EnglishUS);
|
||||
|
||||
return option
|
||||
.setName(t`bot.common.options.username`)
|
||||
.setNameLocalizations(localizationObject('bot.common.options.username'))
|
||||
.setDescription(t`bot.common.options.username.description`)
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject('bot.common.options.username.description'),
|
||||
)
|
||||
.setAutocomplete(true);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const DISCORD_CHOICES_MAX_NAME_SIZE = 25;
|
||||
export const OVERFLOW_SUFFIX = '…';
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
ApplicationCommandOptionChoiceData,
|
||||
AutocompleteInteraction,
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import searchClansAcrossRegions from '../../../../../website/src/core/blitz/searchClansAcrossRegions';
|
||||
import { translator } from '../../localization/translator';
|
||||
import { DISCORD_CHOICES_MAX_NAME_SIZE, OVERFLOW_SUFFIX } from './constants';
|
||||
|
||||
export default async function autocompleteClan(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
) {
|
||||
const { translate } = 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} (${translate(
|
||||
`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} (${translate(
|
||||
`common.regions.short.${clan.region}`,
|
||||
)})`;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value: `${clan.region}/${clan.clan_id}`,
|
||||
} satisfies ApplicationCommandOptionChoiceData<string>;
|
||||
})
|
||||
: [],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
ApplicationCommandOptionChoiceData,
|
||||
AutocompleteInteraction,
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import {
|
||||
DISCORD_CHOICES_MAX_NAME_SIZE,
|
||||
OVERFLOW_SUFFIX,
|
||||
} from './autocompleteClan/constants';
|
||||
import { tankNames } from '@blitzkit/core';
|
||||
|
||||
export const tankNamesTechTreeOnly = tankNames.then((names) =>
|
||||
names.filter((tank) => tank.treeType === 'researchable'),
|
||||
);
|
||||
|
||||
export default async function autocompleteTanks(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
techTreeOnly = false,
|
||||
fields = ['tank'],
|
||||
) {
|
||||
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: ['searchableName', 'searchableNameDeburr', 'camouflages'],
|
||||
limit: 10,
|
||||
},
|
||||
).map(async (item) => {
|
||||
let name = item.obj.searchableName;
|
||||
|
||||
if (name.length > DISCORD_CHOICES_MAX_NAME_SIZE) {
|
||||
const overSize = name.length - DISCORD_CHOICES_MAX_NAME_SIZE;
|
||||
|
||||
name = `${item.obj.searchableName.slice(
|
||||
0,
|
||||
item.obj.searchableName.length -
|
||||
overSize -
|
||||
OVERFLOW_SUFFIX.length,
|
||||
)}${OVERFLOW_SUFFIX}`;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value: `${item.obj.id}`,
|
||||
} satisfies ApplicationCommandOptionChoiceData<string>;
|
||||
}),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
ApplicationCommandOptionChoiceData,
|
||||
AutocompleteInteraction,
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import searchPlayersAcrossRegions from '../../../../website/src/core/blitz/searchPlayersAcrossRegions';
|
||||
import { translator } from '../localization/translator';
|
||||
import {
|
||||
DISCORD_CHOICES_MAX_NAME_SIZE,
|
||||
OVERFLOW_SUFFIX,
|
||||
} from './autocompleteClan/constants';
|
||||
|
||||
export default async function autocompleteUsername(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
) {
|
||||
const { translate } = 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} (${translate(
|
||||
`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} (${translate(
|
||||
`common.regions.short.${player.region}`,
|
||||
)})`;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value: `${player.region}/${player.account_id}`,
|
||||
} satisfies ApplicationCommandOptionChoiceData<string>;
|
||||
})
|
||||
: [],
|
||||
);
|
||||
} catch (error) {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ComponentEmojiResolvable,
|
||||
} from 'discord.js';
|
||||
|
||||
export default 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;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ComponentEmojiResolvable,
|
||||
} from 'discord.js';
|
||||
|
||||
export default 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;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ChatInputCommandInteraction } from 'discord.js';
|
||||
import { translator } from '../localization/translator';
|
||||
import buttonPrimary from './buttonPrimary';
|
||||
|
||||
export function buttonRefresh(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
path: string,
|
||||
) {
|
||||
const { t } = translator(interaction.locale);
|
||||
|
||||
if (
|
||||
interaction.appPermissions?.has('ReadMessageHistory') &&
|
||||
interaction.appPermissions?.has('ViewChannel')
|
||||
) {
|
||||
return buttonPrimary(path, t`bot.common.buttons.refresh`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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('ready', ready)
|
||||
.on('interactionCreate', interactionCreate);
|
||||
@@ -0,0 +1,27 @@
|
||||
import { 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 default function commandToURL(
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
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}` : ''}`;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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('-', '_');
|
||||
const { translate } = translator(Locale.EnglishUS);
|
||||
const slashCommand = new SlashCommandBuilder()
|
||||
.setName(command)
|
||||
.setNameLocalizations(localizationObject(`bot.commands.${commandPathItem}`))
|
||||
.setDescription(translate(`bot.commands.${commandPathItem}.description`))
|
||||
.setDescriptionLocalizations(
|
||||
localizationObject(`bot.commands.${commandPathItem}.description`),
|
||||
);
|
||||
|
||||
if (sub) {
|
||||
sub.map((subItem) => {
|
||||
if ('subcommand' in subItem) {
|
||||
const subcommandNameLocalizations = localizationObject(
|
||||
`bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}`,
|
||||
);
|
||||
const subcommandDescriptionLocalizations = localizationObject(
|
||||
`bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}.description`,
|
||||
);
|
||||
|
||||
slashCommand.addSubcommand((option) => {
|
||||
if (subItem.modify) subItem.modify(option);
|
||||
|
||||
return option
|
||||
.setName(subItem.subcommand)
|
||||
.setNameLocalizations(subcommandNameLocalizations)
|
||||
.setDescription(
|
||||
translate(
|
||||
`bot.commands.${commandPathItem}.subcommands.${subItem.subcommand}.description`,
|
||||
),
|
||||
)
|
||||
.setDescriptionLocalizations(subcommandDescriptionLocalizations);
|
||||
});
|
||||
} else {
|
||||
const subcommandNameLocalizations = localizationObject(
|
||||
`bot.commands.${commandPathItem}.groups.${subItem.group}`,
|
||||
);
|
||||
const subcommandDescriptionLocalizations = localizationObject(
|
||||
`bot.commands.${commandPathItem}.groups.${subItem.group}.description`,
|
||||
);
|
||||
|
||||
slashCommand.addSubcommandGroup((option) => {
|
||||
if (subItem.modify) subItem.modify(option);
|
||||
|
||||
return option
|
||||
.setName(subItem.group)
|
||||
.setNameLocalizations(subcommandNameLocalizations)
|
||||
.setDescription(
|
||||
translate(
|
||||
`bot.commands.${commandPathItem}.groups.${subItem.group}.description`,
|
||||
),
|
||||
)
|
||||
.setDescriptionLocalizations(subcommandDescriptionLocalizations);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return slashCommand;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { ACCENT_COLOR } from './colors';
|
||||
|
||||
export default function embedInfo(title: string, description: string) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(ACCENT_COLOR)
|
||||
.setDescription(description);
|
||||
|
||||
if (title) embed.setTitle(title);
|
||||
|
||||
return embed;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { NEGATIVE_COLOR } from './colors';
|
||||
|
||||
export default function embedNegative(title: string, description: string) {
|
||||
return new EmbedBuilder()
|
||||
.setColor(NEGATIVE_COLOR)
|
||||
.setTitle(title)
|
||||
.setDescription(description);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { POSITIVE_COLOR } from './colors';
|
||||
|
||||
export default function embedPositive(title: string, description: string) {
|
||||
return new EmbedBuilder()
|
||||
.setColor(POSITIVE_COLOR)
|
||||
.setTitle(title)
|
||||
.setDescription(description);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { WARNING_COLOR } from './colors';
|
||||
|
||||
export default function embedWarning(title: string, description: string) {
|
||||
return new EmbedBuilder()
|
||||
.setColor(WARNING_COLOR)
|
||||
.setTitle(title)
|
||||
.setDescription(description);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ChatInputCommandInteraction } from 'discord.js';
|
||||
import { 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;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { StatFilters, TreeType } from '@blitzkit/core';
|
||||
import { ButtonInteraction } from 'discord.js';
|
||||
|
||||
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,
|
||||
tankType: url.searchParams.get('tank-class') ?? undefined,
|
||||
treeType:
|
||||
(url.searchParams.get('tree-type') as TreeType | undefined) ?? undefined,
|
||||
tier: parseInt(url.searchParams.get('tier') ?? '0') || undefined,
|
||||
} satisfies StatFilters;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { StatFilters, TreeType } from '@blitzkit/core';
|
||||
import { ChatInputCommandInteraction } from 'discord.js';
|
||||
import resolveTankId from '../../../../website/src/core/blitz/resolveTankId';
|
||||
|
||||
export async function getFiltersFromCommand(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
) {
|
||||
const tankRaw = interaction.options.getString('tank');
|
||||
|
||||
return {
|
||||
nation: interaction.options.getString('nation') ?? undefined,
|
||||
tier: parseInt(interaction.options.getString('tier') ?? '0') || undefined,
|
||||
tankType: interaction.options.getString('tank-class') ?? undefined,
|
||||
treeType:
|
||||
(interaction.options.getString('tree-type') as TreeType | undefined) ??
|
||||
undefined,
|
||||
tank:
|
||||
tankRaw === null
|
||||
? undefined
|
||||
: await resolveTankId(tankRaw, interaction.locale),
|
||||
} satisfies StatFilters;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Locale } from 'discord.js';
|
||||
import { translator } from '../localization/translator';
|
||||
import { PeriodSize } from './addPeriodSubCommands';
|
||||
|
||||
export function getPeriodOptionName(period: PeriodSize, locale: Locale) {
|
||||
const { translate } = translator(locale);
|
||||
if (period === 'career') return translate('bot.common.periods.career');
|
||||
if (period === 'today') return translate('bot.common.periods.today');
|
||||
return translate('bot.common.periods.days', [period]);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Locale } from 'discord.js';
|
||||
import { SUPPORTED_LOCALES } from '../localization/strings/constants';
|
||||
import { translator } from '../localization/translator';
|
||||
|
||||
export function localizationObject(path: string, literals?: string[]) {
|
||||
return SUPPORTED_LOCALES.reduce<Partial<Record<Locale, string>>>(
|
||||
(localizations, locale) => {
|
||||
const { translate } = translator(locale);
|
||||
return {
|
||||
...localizations,
|
||||
[locale]: translate(path, literals),
|
||||
};
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { assertSecret } from '@blitzkit/core';
|
||||
import { ShardingManager } from 'discord.js';
|
||||
|
||||
export const manager = new ShardingManager('dist/bot/workers/bot.js', {
|
||||
token: assertSecret(process.env.DISCORD_TOKEN),
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
export type TableInputEntry = [string, string | number] | [];
|
||||
export type TableInput = TableInputEntry[];
|
||||
|
||||
export default function markdownTable(input: TableInput) {
|
||||
return input
|
||||
.filter((array) => array[1] !== undefined)
|
||||
.map((array) => (array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`))
|
||||
.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
AttachmentBuilder,
|
||||
ButtonBuilder,
|
||||
EmbedBuilder,
|
||||
InteractionEditReplyOptions,
|
||||
InteractionReplyOptions,
|
||||
MessageEditOptions,
|
||||
} from 'discord.js';
|
||||
import { RenderConfiguration } from '../../../../website/src/core/blitzkit/renderConfiguration';
|
||||
import { InteractionReturnable } from '../../events/interactionCreate';
|
||||
import { Writeable } from '../../types/writable';
|
||||
import jsxToPngThreaded from '../blitzkit/jsxToPngThreaded';
|
||||
|
||||
export default 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<typeof reply.embeds>).push(item);
|
||||
} else if (item instanceof ButtonBuilder) {
|
||||
if (!reply.components)
|
||||
reply.components = [new ActionRowBuilder<ButtonBuilder>()];
|
||||
(reply.components[0] as ActionRowBuilder<ButtonBuilder>).addComponents(
|
||||
item,
|
||||
);
|
||||
} else if (item instanceof AttachmentBuilder) {
|
||||
if (!reply.files) reply.files = [];
|
||||
(reply.files as Writeable<typeof reply.files>).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<typeof reply.files>)?.push(image),
|
||||
);
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Region } from '@blitzkit/core';
|
||||
import { ChatInputCommandInteraction } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import searchClansAcrossRegions from '../../../../website/src/core/blitz/searchClansAcrossRegions';
|
||||
import { UserError } from '../../../../website/src/hooks/userError';
|
||||
import { translator } from '../localization/translator';
|
||||
import { serverAndIdPattern } from './resolvePlayerFromCommand/constants';
|
||||
|
||||
export default async function resolveClanFromCommand(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
) {
|
||||
const { translate } = 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 searchClansAcrossRegions(clan);
|
||||
|
||||
if (accounts[0]) {
|
||||
return { region: accounts[0].region, id: accounts[0].clan_id };
|
||||
} else {
|
||||
throw new UserError(
|
||||
translate('bot.common.errors.clan_not_found', [markdownEscape(clan)]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Region } from '@blitzkit/core';
|
||||
import { ButtonInteraction } from 'discord.js';
|
||||
import getTimeDaysAgo from '../../../../website/src/core/blitzkit/getTimeDaysAgo';
|
||||
import getPeriodNow from '../blitzkit/getPeriodNow';
|
||||
import getPeriodStart from '../blitzkit/getPeriodStart';
|
||||
import { PeriodType } from './addPeriodSubCommands';
|
||||
import { getPeriodOptionName } from './getPeriodOptionName';
|
||||
import { ResolvedPeriod } from './resolvePeriodFromCommand';
|
||||
|
||||
export default 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;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Region } from '@blitzkit/core';
|
||||
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||
import getTimeDaysAgo from '../../../../website/src/core/blitzkit/getTimeDaysAgo';
|
||||
import getPeriodNow from '../blitzkit/getPeriodNow';
|
||||
import getPeriodStart from '../blitzkit/getPeriodStart';
|
||||
import { translator } from '../localization/translator';
|
||||
import { PeriodType } from './addPeriodSubCommands';
|
||||
import { getPeriodOptionName } from './getPeriodOptionName';
|
||||
|
||||
export interface ResolvedPeriod {
|
||||
name: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export default function resolvePeriodFromCommand(
|
||||
region: Region,
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
forcedPeriod?: PeriodType,
|
||||
) {
|
||||
const { translate } = 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 = translate('bot.common.periods.custom', [
|
||||
`${startDaysAgoMin}`,
|
||||
`${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;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Region } from '@blitzkit/core';
|
||||
import { ButtonInteraction } from 'discord.js';
|
||||
import { ResolvedPlayer } from './resolvePlayerFromCommand';
|
||||
|
||||
export default 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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Region } from '@blitzkit/core';
|
||||
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||
import markdownEscape from 'markdown-escape';
|
||||
import searchPlayersAcrossRegions from '../../../../../website/src/core/blitz/searchPlayersAcrossRegions';
|
||||
import { UserError } from '../../../../../website/src/hooks/userError';
|
||||
import { getBlitzFromDiscord } from '../../blitzkit/getBlitzFromDiscord';
|
||||
import { translator } from '../../localization/translator';
|
||||
import { serverAndIdPattern } from './constants';
|
||||
|
||||
export interface ResolvedPlayer {
|
||||
region: Region;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default async function resolvePlayerFromCommand(
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
) {
|
||||
const { t, translate } = translator(interaction.locale);
|
||||
const commandUsername = interaction.options.getString('username');
|
||||
|
||||
if (commandUsername) {
|
||||
if (serverAndIdPattern.test(commandUsername)) {
|
||||
const [server, accountId] = commandUsername.split('/');
|
||||
|
||||
return {
|
||||
region: server as Region,
|
||||
id: Number(accountId),
|
||||
} satisfies ResolvedPlayer;
|
||||
} else {
|
||||
const accounts = await searchPlayersAcrossRegions(commandUsername);
|
||||
|
||||
if (accounts[0]) {
|
||||
return {
|
||||
region: accounts[0].region,
|
||||
id: accounts[0].account_id,
|
||||
} satisfies ResolvedPlayer;
|
||||
} else {
|
||||
throw new UserError(
|
||||
translate('bot.common.errors.player_not_found', [
|
||||
markdownEscape(commandUsername),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const account = await getBlitzFromDiscord(BigInt(interaction.user.id));
|
||||
|
||||
if (account) return account;
|
||||
|
||||
throw new UserError(t`bot.common.errors.player_not_linked`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Locale } from 'discord.js';
|
||||
|
||||
export const SUPPORTED_LOCALES: Locale[] = [Locale.EnglishUS, Locale.SpanishES];
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Locale } from 'discord.js';
|
||||
import { SUPPORTED_LOCALES } from './constants';
|
||||
|
||||
export type TranslationFragmentTree = {
|
||||
[key: string]: TranslationFragment;
|
||||
} & {
|
||||
$?: string;
|
||||
};
|
||||
export type TranslationFragment = string | TranslationFragmentTree;
|
||||
|
||||
export const translations = SUPPORTED_LOCALES.reduce<
|
||||
Partial<Record<Locale, TranslationFragment>>
|
||||
>(
|
||||
(table, locale) => ({
|
||||
...table,
|
||||
[locale]: require(`../lang/${locale}.json`),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Locale } from 'discord.js';
|
||||
import { translations } from './strings';
|
||||
import { SUPPORTED_LOCALES } from './strings/constants';
|
||||
|
||||
export function translator(localeRaw: Locale) {
|
||||
const locale =
|
||||
localeRaw !== Locale.EnglishUS && SUPPORTED_LOCALES.includes(localeRaw)
|
||||
? localeRaw
|
||||
: Locale.EnglishUS;
|
||||
const strings = translations[locale]!;
|
||||
|
||||
function translate(path: string, literals?: string[]): string {
|
||||
const pathArray = path.split('.');
|
||||
let fragment = strings;
|
||||
let resolvedString: string;
|
||||
|
||||
for (const pathItem of pathArray) {
|
||||
if (typeof fragment === 'string') {
|
||||
throw new SyntaxError(
|
||||
`Attempted to access string with key "${pathItem}" in "${path}" from within a string instead of an object in locale "${locale}"`,
|
||||
);
|
||||
} else {
|
||||
fragment = fragment[pathItem];
|
||||
|
||||
if (typeof fragment === 'undefined') {
|
||||
const message = `Undefined translation at "${pathItem}" in "${path}" for locale "${locale}"`;
|
||||
|
||||
if (locale === Locale.EnglishUS) {
|
||||
throw new Error(message);
|
||||
} else {
|
||||
console.warn(`${message} falling back to en-US`);
|
||||
return translator(Locale.EnglishUS).translate(path, literals);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof fragment === 'string') {
|
||||
resolvedString = fragment;
|
||||
} else if (fragment.$) {
|
||||
resolvedString = fragment.$!;
|
||||
} else {
|
||||
throw new Error(
|
||||
`Unresolved tree ending for "${path}" in locale "${locale}"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (literals) {
|
||||
const chunks = resolvedString.split('%s');
|
||||
let embeddedString = '';
|
||||
|
||||
chunks.forEach((chunk, index) => {
|
||||
if (index !== 0) embeddedString += literals[index - 1];
|
||||
embeddedString += chunk;
|
||||
});
|
||||
|
||||
return embeddedString;
|
||||
}
|
||||
|
||||
return resolvedString;
|
||||
}
|
||||
|
||||
function t(paths: TemplateStringsArray, ...embeds: string[]) {
|
||||
return paths
|
||||
.map((path, index) =>
|
||||
path.length === 0
|
||||
? ''
|
||||
: `${translate(path)}${index === embeds.length ? '' : embeds[index]}`,
|
||||
)
|
||||
.join('');
|
||||
}
|
||||
|
||||
return { locale, translate, t };
|
||||
}
|
||||
Reference in new issue
Block a user