mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-23 19:49:50 +02:00
feat: organized files
This commit is contained in:
1 parent
f2ae698438
commit
7dd3913ae8
51 files changed
+68
-68
No files matched your search
@@ -0,0 +1,10 @@
|
||||
import { SlashCommandStringOption } from 'discord.js';
|
||||
|
||||
export default function addClanChoices(option: SlashCommandStringOption) {
|
||||
return option
|
||||
.setName('clan')
|
||||
.setDescription('The clan name or tag you are checking')
|
||||
.setAutocomplete(true)
|
||||
.setMinLength(2)
|
||||
.setRequired(true);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
SlashCommandSubcommandBuilder,
|
||||
SlashCommandSubcommandGroupBuilder,
|
||||
} from 'discord.js';
|
||||
import { PERIOD_NAMES } from '../discord/resolvePeriodFromCommand.js';
|
||||
|
||||
export type Period = 'today' | '30' | '60' | '90' | 'career' | 'custom';
|
||||
|
||||
export default function addPeriodSubCommands(
|
||||
option: SlashCommandSubcommandGroupBuilder,
|
||||
extra: (
|
||||
option: SlashCommandSubcommandBuilder,
|
||||
) => SlashCommandSubcommandBuilder = (option) => option,
|
||||
) {
|
||||
return option
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('today').setDescription(PERIOD_NAMES.today)),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('30').setDescription(PERIOD_NAMES[30])),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('60').setDescription(PERIOD_NAMES[60])),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('90').setDescription(PERIOD_NAMES[90])),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(option.setName('career').setDescription(PERIOD_NAMES.career)),
|
||||
)
|
||||
.addSubcommand((option) =>
|
||||
extra(
|
||||
option
|
||||
.setName('custom')
|
||||
.setDescription(PERIOD_NAMES.custom)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('start')
|
||||
.setDescription('Start of period in days ago from today')
|
||||
.setMinValue(0)
|
||||
.setRequired(true),
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('end')
|
||||
.setDescription('End of period in days ago from today')
|
||||
.setMinValue(0)
|
||||
.setRequired(true),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { SlashCommandStringOption } from 'discord.js';
|
||||
import { BLITZ_SERVERS } from '../../constants/servers.js';
|
||||
|
||||
export default function addServerChoices(option: SlashCommandStringOption) {
|
||||
return option
|
||||
.setName('server')
|
||||
.setDescription('The Blitz server you are in')
|
||||
.addChoices(
|
||||
{ name: BLITZ_SERVERS.com, value: 'com' },
|
||||
{ name: BLITZ_SERVERS.eu, value: 'eu' },
|
||||
{ name: BLITZ_SERVERS.asia, value: 'asia' },
|
||||
)
|
||||
.setRequired(true);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { SlashCommandBuilder } from 'discord.js';
|
||||
import addPeriodSubCommands from './addPeriodSubCommands.js';
|
||||
import addTankChoices from './addTankChoices.js';
|
||||
import addUsernameOption from './addUsernameOption.js';
|
||||
|
||||
export default function addStatsSubCommandGroups(option: SlashCommandBuilder) {
|
||||
return option
|
||||
.addSubcommandGroup((option) =>
|
||||
addPeriodSubCommands(option, (option) =>
|
||||
option.addStringOption(addUsernameOption),
|
||||
)
|
||||
.setName('player')
|
||||
.setDescription("Player's statistics"),
|
||||
)
|
||||
.addSubcommandGroup((option) =>
|
||||
addPeriodSubCommands(option, (option) =>
|
||||
option
|
||||
.addStringOption(addTankChoices)
|
||||
.addStringOption(addUsernameOption),
|
||||
)
|
||||
.setName('tank')
|
||||
.setDescription("Tank's statistics"),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SlashCommandStringOption } from 'discord.js';
|
||||
|
||||
export default function addTankChoices(option: SlashCommandStringOption) {
|
||||
return option
|
||||
.setName('tank')
|
||||
.setDescription('The tank to get stats for')
|
||||
.setAutocomplete(true)
|
||||
.setRequired(true);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { SlashCommandStringOption } from 'discord.js';
|
||||
|
||||
export default function addUsernameOption(option: SlashCommandStringOption) {
|
||||
return option
|
||||
.setName('username')
|
||||
.setDescription(
|
||||
'The username you use in Blitz (inferred from Discord nickname by default)',
|
||||
)
|
||||
.setAutocomplete(true);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
ApplicationCommandOptionChoiceData,
|
||||
AutocompleteInteraction,
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import { BLITZ_SERVERS } from '../../constants/servers.js';
|
||||
import listClansPanServer from '../blitz/listClansPanServer.js';
|
||||
|
||||
export default async function clanAutocomplete(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
) {
|
||||
const focusedOption = interaction.options.getFocused(true);
|
||||
if (focusedOption.name !== 'clan') return;
|
||||
const players = await listClansPanServer(focusedOption.value);
|
||||
|
||||
await interaction.respond(
|
||||
players
|
||||
? players.map(
|
||||
(player) =>
|
||||
({
|
||||
name: `${player.name} (${BLITZ_SERVERS[player.server]})`,
|
||||
value: `${player.server}/${player.clan_id}`,
|
||||
} satisfies ApplicationCommandOptionChoiceData<string>),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
|
||||
console.log(`Clan autocomplete for ${focusedOption.value}`);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export type TableInputEntry = [string, string | number] | [];
|
||||
export type TableInput = TableInputEntry[];
|
||||
|
||||
export default function cleanTable(input: TableInput) {
|
||||
return input
|
||||
.filter((array) => array[1] !== undefined)
|
||||
.map((array) => (array.length === 0 ? '' : `**${array[0]}**: ${array[1]}`))
|
||||
.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ButtonBuilder, ButtonStyle } from 'discord.js';
|
||||
import { BlitzServer } from '../../constants/servers.js';
|
||||
|
||||
export default function fullBlitzStarsStats(
|
||||
server: BlitzServer,
|
||||
username: string,
|
||||
) {
|
||||
return new ButtonBuilder()
|
||||
.setLabel('View on BlitzStars')
|
||||
.setURL(`https://www.blitzstars.com/player/${server}/${username}`)
|
||||
.setStyle(ButtonStyle.Link);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||
|
||||
export default function generateCustomId(
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
) {
|
||||
return `${interaction.commandName}/${
|
||||
interaction.options.getSubcommandGroup() ?? ''
|
||||
}/${interaction.options.getSubcommand() ?? ''}`;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { ACCENT_COLOR } from '../../constants/colors.js';
|
||||
|
||||
export default function infoEmbed(title: string, description: string) {
|
||||
return new EmbedBuilder()
|
||||
.setColor(ACCENT_COLOR)
|
||||
.setTitle(title)
|
||||
.setDescription(description);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
|
||||
|
||||
export default function interactionToURL(
|
||||
interaction: ChatInputCommandInteraction<CacheType>,
|
||||
) {
|
||||
const pathname = [
|
||||
interaction.commandName,
|
||||
interaction.options.getSubcommandGroup(),
|
||||
interaction.options.getSubcommand(),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('/');
|
||||
const urlSearchParams = new URLSearchParams();
|
||||
|
||||
interaction.options.data.forEach((option) => {
|
||||
if (option.value) urlSearchParams.set(option.name, `${option.value}`);
|
||||
});
|
||||
|
||||
console.log(`${urlSearchParams}`);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ComponentEmojiResolvable,
|
||||
} from 'discord.js';
|
||||
import { CYCLIC_API } from '../../constants/cyclic.js';
|
||||
|
||||
export default function linkButton(
|
||||
id: string,
|
||||
label: string,
|
||||
emoji?: ComponentEmojiResolvable,
|
||||
) {
|
||||
const button = new ButtonBuilder()
|
||||
.setStyle(ButtonStyle.Link)
|
||||
.setURL(`${CYCLIC_API}/${id}`)
|
||||
.setLabel(label);
|
||||
|
||||
if (emoji) button.setEmoji(emoji);
|
||||
|
||||
return button;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { NEGATIVE_COLOR } from '../../constants/colors.js';
|
||||
|
||||
export default function negativeEmbed(title: string, description: string) {
|
||||
return new EmbedBuilder()
|
||||
.setColor(NEGATIVE_COLOR)
|
||||
.setTitle(title)
|
||||
.setDescription(description);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import negativeEmbed from './negativeEmbed.js';
|
||||
|
||||
export const noMember = negativeEmbed(
|
||||
'User of command is not a member',
|
||||
'Discord claims that the user of this command does not have an account... strange!',
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
EmbedBuilder,
|
||||
InteractionEditReplyOptions,
|
||||
} from 'discord.js';
|
||||
import { InteractionReturnable } from '../../events/interactionCreate/index.js';
|
||||
import render from '../ui/render.js';
|
||||
|
||||
export default async function normalizeInteractionReturnable(
|
||||
returnable: InteractionReturnable,
|
||||
) {
|
||||
const reply: InteractionEditReplyOptions = {};
|
||||
|
||||
const awaitedReturnable = await returnable;
|
||||
const normalizedReturnable = Array.isArray(awaitedReturnable)
|
||||
? awaitedReturnable
|
||||
: [awaitedReturnable];
|
||||
|
||||
await Promise.all(
|
||||
normalizedReturnable.map(async (item) => {
|
||||
if (item instanceof EmbedBuilder) {
|
||||
if (!reply.embeds) reply.embeds = [];
|
||||
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 {
|
||||
const image = await render(item);
|
||||
if (!reply.files) reply.files = [];
|
||||
reply.files.push(image);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return reply;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import negativeEmbed from './negativeEmbed.js';
|
||||
|
||||
export const notTrackedByBlitzStars = negativeEmbed(
|
||||
'No data to display',
|
||||
'Player is not tracked by BlitzStars.',
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { POSITIVE_COLOR } from '../../constants/colors.js';
|
||||
|
||||
export default function positiveEmbed(title: string, description: string) {
|
||||
return new EmbedBuilder()
|
||||
.setColor(POSITIVE_COLOR)
|
||||
.setTitle(title)
|
||||
.setDescription(description);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ComponentEmojiResolvable,
|
||||
} from 'discord.js';
|
||||
|
||||
export default function primaryButton(
|
||||
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,92 @@
|
||||
import {
|
||||
ButtonInteraction,
|
||||
CacheType,
|
||||
ChatInputCommandInteraction,
|
||||
} from 'discord.js';
|
||||
import { Request } from 'express';
|
||||
import { CYCLIC_API } from '../../constants/cyclic.js';
|
||||
import getPeriodNow from '../blitzstars/getPeriodNow.js';
|
||||
import getPeriodStart from '../blitzstars/getPeriodStart.js';
|
||||
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo.js';
|
||||
import { Period } from '../discord/addPeriodSubCommands.js';
|
||||
|
||||
export const PERIOD_NAMES: Record<Period, string> = {
|
||||
today: "Today's statistics",
|
||||
30: "30 days' statistics",
|
||||
60: "60 days' statistics",
|
||||
90: "90 days' statistics",
|
||||
career: 'Career statistics',
|
||||
custom: 'Custom period',
|
||||
};
|
||||
|
||||
export const EVOLUTION_PERIOD_NAMES: Record<Period, string> = {
|
||||
today: "Today's evolution",
|
||||
30: "30 days' evolution",
|
||||
60: "60 days' evolution",
|
||||
90: "90 days' evolution",
|
||||
career: 'Career evolution',
|
||||
custom: 'Custom period',
|
||||
};
|
||||
|
||||
export interface ResolvedPeriod {
|
||||
statsName: string;
|
||||
evolutionName: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export default function resolvePeriodFromCommand(
|
||||
input:
|
||||
| ChatInputCommandInteraction<CacheType>
|
||||
| ButtonInteraction<CacheType>
|
||||
| Request,
|
||||
) {
|
||||
let statsName: string;
|
||||
let evolutionName: string;
|
||||
let start: number;
|
||||
let end: number;
|
||||
let url: URL;
|
||||
let period: Period;
|
||||
|
||||
if (input instanceof ChatInputCommandInteraction) {
|
||||
period = input.options.getSubcommand() as Period;
|
||||
} else {
|
||||
url = new URL(
|
||||
`${CYCLIC_API}${
|
||||
input instanceof ButtonInteraction ? `/${input.customId}` : input.path
|
||||
}`,
|
||||
);
|
||||
const path = url.pathname.split('/').filter(Boolean);
|
||||
period = path[path.length - 1] as Period;
|
||||
}
|
||||
|
||||
if (period === 'custom') {
|
||||
const startRaw =
|
||||
input instanceof ChatInputCommandInteraction
|
||||
? input.options.getInteger('start')!
|
||||
: parseInt(url!.searchParams.get('start')!);
|
||||
const endRaw =
|
||||
input instanceof ChatInputCommandInteraction
|
||||
? input.options.getInteger('end')!
|
||||
: parseInt(url!.searchParams.get('end')!);
|
||||
const startMin = Math.min(startRaw, endRaw);
|
||||
const endMax = Math.max(startRaw, endRaw);
|
||||
|
||||
statsName = `${startMin} to ${endMax} days' statistics`;
|
||||
evolutionName = `${startMin} to ${endMax} days' evolution`;
|
||||
start = getTimeDaysAgo(startMin);
|
||||
end = getTimeDaysAgo(endMax);
|
||||
} else {
|
||||
statsName = PERIOD_NAMES[period];
|
||||
evolutionName = EVOLUTION_PERIOD_NAMES[period];
|
||||
start = getPeriodStart(period);
|
||||
end = getPeriodNow();
|
||||
}
|
||||
|
||||
return {
|
||||
statsName,
|
||||
evolutionName,
|
||||
start,
|
||||
end,
|
||||
} satisfies ResolvedPeriod;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
ButtonInteraction,
|
||||
CacheType,
|
||||
ChatInputCommandInteraction,
|
||||
} from 'discord.js';
|
||||
import { CYCLIC_API } from '../../constants/cyclic.js';
|
||||
import { BlitzServer } from '../../constants/servers.js';
|
||||
import listAccountsPanServer, {
|
||||
usernamePatternWithoutPosition,
|
||||
} from '../blitz/listAccountsPanServer.js';
|
||||
import errorWithCause from '../node/errorWithCause.js';
|
||||
|
||||
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
|
||||
|
||||
export interface ResolvedPlayer {
|
||||
server: BlitzServer;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default async function resolvePlayer(
|
||||
interaction:
|
||||
| ChatInputCommandInteraction<CacheType>
|
||||
| ButtonInteraction<CacheType>,
|
||||
) {
|
||||
if (interaction instanceof ChatInputCommandInteraction) {
|
||||
const commandUsername = interaction.options.getString('username');
|
||||
const displayName = interaction.guild?.members.cache
|
||||
.get(interaction.user.id)
|
||||
?.displayName.match(usernamePatternWithoutPosition)?.[0];
|
||||
const username = commandUsername ?? displayName;
|
||||
|
||||
if (username === undefined) {
|
||||
throw errorWithCause(
|
||||
'Username could not be inferred',
|
||||
"You didn't provide me a username in the command nor was I able to infer your Blitz username from your Discord nickname. Try providing a username manually in the command or using the `/verify` command to set your Discord nickname to your Blitz username.",
|
||||
);
|
||||
}
|
||||
|
||||
if (serverAndIdPattern.test(username)) {
|
||||
const [server, accountId] = username.split('/');
|
||||
return {
|
||||
server: server as BlitzServer,
|
||||
id: Number(accountId),
|
||||
} satisfies ResolvedPlayer;
|
||||
} else {
|
||||
const accounts = await listAccountsPanServer(username);
|
||||
|
||||
if (accounts[0]) {
|
||||
return {
|
||||
server: accounts[0].server,
|
||||
id: accounts[0].account_id,
|
||||
} satisfies ResolvedPlayer;
|
||||
} else {
|
||||
throw errorWithCause(
|
||||
'Could not find user',
|
||||
`I couldn't find user \`${username}\`. Try providing a valid username manually or using the \`/verify\` command to set your Discord nickname to your Blitz username.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const url = new URL(`${CYCLIC_API}/${interaction.customId}`);
|
||||
return {
|
||||
id: parseInt(url.searchParams.get('id')!),
|
||||
server: url.searchParams.get('server') as BlitzServer,
|
||||
} satisfies ResolvedPlayer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { ButtonBuilder, ButtonStyle } from 'discord.js';
|
||||
|
||||
export const supportBlitzStars = new ButtonBuilder()
|
||||
.setLabel('Support BlitzStars')
|
||||
.setURL('https://www.blitzstars.com/supporters')
|
||||
.setStyle(ButtonStyle.Link);
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
ApplicationCommandOptionChoiceData,
|
||||
AutocompleteInteraction,
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import { go } from 'fuzzysort';
|
||||
import resolveTankName from '../blitz/resolveTankName.js';
|
||||
import { TANKS } from '../blitz/tankopedia.js';
|
||||
|
||||
export default async function tanksAutocomplete(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
) {
|
||||
const focusedOption = interaction.options.getFocused(true);
|
||||
if (focusedOption.name !== 'tank') return;
|
||||
|
||||
await interaction.respond(
|
||||
focusedOption.value
|
||||
? go(focusedOption.value, TANKS, { keys: ['name'], limit: 10 }).map(
|
||||
(item) =>
|
||||
({
|
||||
name: item.obj.name
|
||||
? item.obj.name
|
||||
: resolveTankName(item.obj.tank_id),
|
||||
value: `${item.obj.tank_id}`,
|
||||
} satisfies ApplicationCommandOptionChoiceData<string>),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
|
||||
console.log(`Tanks autocomplete for ${focusedOption.value}`);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
ApplicationCommandOptionChoiceData,
|
||||
AutocompleteInteraction,
|
||||
CacheType,
|
||||
} from 'discord.js';
|
||||
import { BLITZ_SERVERS } from '../../constants/servers.js';
|
||||
import listAccountsPanServer from '../blitz/listAccountsPanServer.js';
|
||||
|
||||
export default async function usernameAutocomplete(
|
||||
interaction: AutocompleteInteraction<CacheType>,
|
||||
) {
|
||||
const focusedOption = interaction.options.getFocused(true);
|
||||
if (focusedOption.name !== 'username') return;
|
||||
const players = await listAccountsPanServer(focusedOption.value);
|
||||
|
||||
try {
|
||||
await interaction.respond(
|
||||
players
|
||||
? players.map(
|
||||
(player) =>
|
||||
({
|
||||
name: `${player.nickname} (${BLITZ_SERVERS[player.server]})`,
|
||||
value: `${player.server}/${player.account_id}`,
|
||||
} satisfies ApplicationCommandOptionChoiceData<string>),
|
||||
)
|
||||
: [],
|
||||
);
|
||||
|
||||
console.log(`Username autocomplete for ${focusedOption.value}`);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to autocomplete username for ${focusedOption.value} in time`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { WARNING_COLOR } from '../../constants/colors.js';
|
||||
|
||||
export default function warningEmbed(title: string, description: string) {
|
||||
return new EmbedBuilder()
|
||||
.setColor(WARNING_COLOR)
|
||||
.setTitle(title)
|
||||
.setDescription(description);
|
||||
}
|
||||
Reference in new issue
Block a user