feat: organized files

This commit is contained in:
TresAbhi committed 2023-06-27 01:46:08 +00:00
1 parent f2ae698438
commit 7dd3913ae8
51 files changed
+68 -68

No files matched your search

+10
View File
@@ -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);
}
+52
View File
@@ -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),
),
),
);
}
+14
View File
@@ -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"),
);
}
+9
View File
@@ -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);
}
+10
View File
@@ -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);
}
+29
View File
@@ -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}`);
}
+9
View File
@@ -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');
}
+12
View File
@@ -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);
}
+9
View File
@@ -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() ?? ''}`;
}
+9
View File
@@ -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);
}
+20
View File
@@ -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}`);
}
+21
View File
@@ -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;
}
+9
View File
@@ -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);
}
+6
View File
@@ -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.',
);
+9
View File
@@ -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);
}
+20
View File
@@ -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;
}
+67
View File
@@ -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;
}
}
+6
View File
@@ -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);
+31
View File
@@ -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}`);
}
+35
View File
@@ -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`,
);
}
}
+9
View File
@@ -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);
}