feat: swap core and _core

This commit is contained in:
TrèsAbhi committed 2023-10-14 11:06:03 -05:00
1 parent 07e3660388
commit b827f7f730
113 files changed
+254 -247

No files matched your search

-10
View File
@@ -1,10 +0,0 @@
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);
}
-51
View File
@@ -1,51 +0,0 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
export type PeriodType = 'today' | '30' | '60' | '90' | 'career' | 'custom';
export type PeriodSize = 'today' | `${number}` | 'career';
export default function addPeriodSubCommands(
option: SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
extra: (
option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option,
) {
option
.addSubcommand((option) =>
extra(option.setName('today').setDescription('Today')),
)
.addSubcommand((option) =>
extra(option.setName('30').setDescription('30 days')),
)
.addSubcommand((option) =>
extra(option.setName('60').setDescription('60 days')),
)
.addSubcommand((option) =>
extra(option.setName('90').setDescription('90 days')),
)
.addSubcommand((option) =>
extra(option.setName('career').setDescription('Career')),
)
.addSubcommand((option) =>
extra(
option
.setName('custom')
.setDescription('Custom range')
.addIntegerOption((option) =>
option
.setName('start')
.setDescription('Start in number of days ago from today')
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('end')
.setDescription('End in number of days')
.setRequired(true),
),
),
);
}
-14
View File
@@ -1,14 +0,0 @@
import { SlashCommandStringOption } from 'discord.js';
import { REGION_NAMES } from '../../constants/regions';
export default function addRegionChoices(option: SlashCommandStringOption) {
return option
.setName('region')
.setDescription('The Blitz region you are in')
.addChoices(
{ name: REGION_NAMES.com, value: 'com' },
{ name: REGION_NAMES.eu, value: 'eu' },
{ name: REGION_NAMES.asia, value: 'asia' },
)
.setRequired(true);
}
@@ -1,76 +0,0 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
SlashCommandSubcommandGroupBuilder,
} from 'discord.js';
import { TREE_TYPE_NAMES, TreeTypeString } from '../../components/Tanks';
import { tankopediaInfo } from '../blitz/tankopedia';
import addPeriodSubCommands from './addPeriodSubCommands';
import addTankChoices from './addTankChoices';
import addTierChoices from './addTierChoices';
export default async function addStatTypeSubCommands<
OptionType extends SlashCommandBuilder | SlashCommandSubcommandGroupBuilder,
>(
option: OptionType,
extra: (
option: SlashCommandSubcommandBuilder,
) => SlashCommandSubcommandBuilder = (option) => option,
): Promise<OptionType> {
const awaitedTankopediaInfo = await tankopediaInfo;
addPeriodSubCommands(option, (option) => {
extra(option);
return option
.addStringOption((option) =>
option
.setName('nation')
.setDescription('Nation')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_nations).map(
([type, name]) => ({ name, value: type }),
),
)
.setRequired(false),
)
.addStringOption((option) => addTierChoices(option).setRequired(false))
.addStringOption((option) =>
option
.setName('tank-type')
.setDescription('Tank type')
.addChoices(
...Object.entries(awaitedTankopediaInfo.vehicle_types).map(
([type, name]) => ({
name: name.replace(' Tank', ''),
value: type,
}),
),
)
.setRequired(false),
)
.addStringOption((option) =>
option
.setName('tree-type')
.setDescription('Tech tree type')
.addChoices(
{
name: TREE_TYPE_NAMES['tech-tree'],
value: 'tech-tree' satisfies TreeTypeString,
},
{
name: TREE_TYPE_NAMES.premium,
value: 'premium' satisfies TreeTypeString,
},
{
name: TREE_TYPE_NAMES.collector,
value: 'collector' satisfies TreeTypeString,
},
)
.setRequired(false),
)
.addStringOption((option) => addTankChoices(option).setRequired(false));
});
return option;
}
-9
View File
@@ -1,9 +0,0 @@
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);
}
-16
View File
@@ -1,16 +0,0 @@
import { SlashCommandStringOption } from 'discord.js';
import { range } from 'lodash';
import numberToRomanNumeral from '../blitz/numberToRomanNumeral';
export default function addTierChoices(option: SlashCommandStringOption) {
return option
.setName('tier')
.setDescription('The tier you want to see')
.setChoices(
...range(10, 0).map((tier) => ({
name: `Tier ${tier} - ${numberToRomanNumeral(tier)}`,
value: `${tier}`,
})),
)
.setRequired(true);
}
-10
View File
@@ -1,10 +0,0 @@
import { SlashCommandStringOption } from 'discord.js';
export default function addUsernameChoices(option: SlashCommandStringOption) {
return option
.setName('username')
.setDescription(
'The username you use in Blitz (inferred from Discord nickname by default)',
)
.setAutocomplete(true);
}
-28
View File
@@ -1,28 +0,0 @@
import {
ApplicationCommandOptionChoiceData,
AutocompleteInteraction,
CacheType,
} from 'discord.js';
import { REGION_NAMES } from '../../constants/regions';
import listClans from '../blitz/listClans';
export default async function autocompleteClan(
interaction: AutocompleteInteraction<CacheType>,
) {
const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'clan') return;
if (focusedOption.value.length < 2) return interaction.respond([]);
const players = await listClans(focusedOption.value);
await interaction.respond(
players
? players.map(
(player) =>
({
name: `${player.name} (${REGION_NAMES[player.region]})`,
value: `${player.region}/${player.clan_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>,
)
: [],
);
}
@@ -1,20 +0,0 @@
import { AutocompleteInteraction, CacheType } from 'discord.js';
import { go } from 'fuzzysort';
import { STAT_PROPERTIES } from '../blitzkrieg/addStatPropertyOptions';
export default async function autocompleteStatProperty(
interaction: AutocompleteInteraction<CacheType>,
name = 'stat-property',
) {
const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== name) return;
await interaction.respond(
focusedOption.value
? go(focusedOption.value, STAT_PROPERTIES, {
keys: ['name'],
limit: 10,
}).map(({ obj }) => obj)
: STAT_PROPERTIES.slice(0, 25),
);
}
-31
View File
@@ -1,31 +0,0 @@
import {
ApplicationCommandOptionChoiceData,
AutocompleteInteraction,
CacheType,
} from 'discord.js';
import { go } from 'fuzzysort';
import { TANK_NAMES_DIACRITICS } from '../blitz/tankopedia';
export default async function autocompleteTanks(
interaction: AutocompleteInteraction<CacheType>,
) {
const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'tank') return;
await interaction.respond(
focusedOption.value
? await Promise.all(
go(focusedOption.value, await TANK_NAMES_DIACRITICS, {
keys: ['combined'],
limit: 10,
}).map(
async (item) =>
({
name: item.obj.original,
value: `${item.obj.id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>,
),
)
: [],
);
}
-29
View File
@@ -1,29 +0,0 @@
import {
ApplicationCommandOptionChoiceData,
AutocompleteInteraction,
CacheType,
} from 'discord.js';
import { REGION_NAMES } from '../../constants/regions';
import listPlayers from '../blitz/listPlayers';
export default async function autocompleteUsername(
interaction: AutocompleteInteraction<CacheType>,
) {
const focusedOption = interaction.options.getFocused(true);
if (focusedOption.name !== 'username') return;
const players = await listPlayers(focusedOption.value);
try {
await interaction.respond(
players
? players.map(
(player) =>
({
name: `${player.nickname} (${REGION_NAMES[player.region]})`,
value: `${player.region}/${player.account_id}`,
}) satisfies ApplicationCommandOptionChoiceData<string>,
)
: [],
);
} catch (error) {}
}
-11
View File
@@ -1,11 +0,0 @@
import { Client, GatewayIntentBits } from 'discord.js';
import guildMemberAdd from '../../events/guildMemberAdd';
import interactionCreate from '../../events/interactionCreate';
import ready from '../../events/ready';
export const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
})
.on('ready', ready)
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate);
-12
View File
@@ -1,12 +0,0 @@
import { EmbedBuilder } from 'discord.js';
import { ACCENT_COLOR } from '../../constants/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;
}
-9
View File
@@ -1,9 +0,0 @@
import { EmbedBuilder } from 'discord.js';
import { NEGATIVE_COLOR } from '../../constants/colors';
export default function embedNegative(title: string, description: string) {
return new EmbedBuilder()
.setColor(NEGATIVE_COLOR)
.setTitle(title)
.setDescription(description);
}
-9
View File
@@ -1,9 +0,0 @@
import { EmbedBuilder } from 'discord.js';
import { POSITIVE_COLOR } from '../../constants/colors';
export default function embedPositive(title: string, description: string) {
return new EmbedBuilder()
.setColor(POSITIVE_COLOR)
.setTitle(title)
.setDescription(description);
}
-9
View File
@@ -1,9 +0,0 @@
import { EmbedBuilder } from 'discord.js';
import { WARNING_COLOR } from '../../constants/colors';
export default function embedWarning(title: string, description: string) {
return new EmbedBuilder()
.setColor(WARNING_COLOR)
.setTitle(title)
.setDescription(description);
}
-14
View File
@@ -1,14 +0,0 @@
import { ChatInputCommandInteraction } from 'discord.js';
import { PeriodType } from './addPeriodSubCommands';
export function getCustomPeriodParams(
interaction: ChatInputCommandInteraction,
) {
if ((interaction.options.getSubcommand() as PeriodType) === 'custom')
return {
start: interaction.options.getInteger('start', true),
end: interaction.options.getInteger('end', true),
};
return undefined;
}
-17
View File
@@ -1,17 +0,0 @@
import { ButtonInteraction } from 'discord.js';
import { StatFilters } from '../../_core/statistics/filterStats';
import { TreeTypeString } from '../../components/Tanks';
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-type') ?? undefined,
treeType:
(url.searchParams.get('tree-type') as TreeTypeString | undefined) ??
undefined,
tier: parseInt(url.searchParams.get('tier') ?? '0') || undefined,
} satisfies StatFilters;
}
-18
View File
@@ -1,18 +0,0 @@
import { ChatInputCommandInteraction } from 'discord.js';
import { StatFilters } from '../../_core/statistics/filterStats';
import { TreeTypeString } from '../../components/Tanks';
export function getFiltersFromCommand(
interaction: ChatInputCommandInteraction,
) {
return {
nation: interaction.options.getString('nation') ?? undefined,
tier: parseInt(interaction.options.getString('tier') ?? '0') || undefined,
tankType: interaction.options.getString('tank-type') ?? undefined,
treeType:
(interaction.options.getString('tree-type') as
| TreeTypeString
| undefined) ?? undefined,
tank: parseInt(interaction.options.getString('tank') ?? '0') || undefined,
} satisfies StatFilters;
}
-24
View File
@@ -1,24 +0,0 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
export interface Parameters {
[key: string]: string | number | boolean | undefined | null;
}
export default function interactionToURL(
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}` : ''}`;
}
-20
View File
@@ -1,20 +0,0 @@
import {
ButtonBuilder,
ButtonStyle,
ComponentEmojiResolvable,
} from 'discord.js';
export default function linkButton(
url: string,
label: string,
emoji?: ComponentEmojiResolvable,
) {
const button = new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setURL(url)
.setLabel(label);
if (emoji) button.setEmoji(emoji);
return button;
}
-9
View File
@@ -1,9 +0,0 @@
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');
}
@@ -1,48 +0,0 @@
import {
ActionRowBuilder,
AttachmentBuilder,
ButtonBuilder,
EmbedBuilder,
InteractionEditReplyOptions,
} from 'discord.js';
import { InteractionReturnable } from '../../events/interactionCreate';
import jsxToPng from '../node/jsxToPng';
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 (typeof item === 'string') {
reply.content = item;
} else 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 if (item instanceof AttachmentBuilder) {
if (!reply.files) reply.files = [];
reply.files.push(item);
} else if (item === null) {
return;
} else {
const image = await jsxToPng(item);
if (!reply.files) reply.files = [];
reply.files.push(image);
}
}),
);
return reply;
}
-20
View File
@@ -1,20 +0,0 @@
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;
}
-20
View File
@@ -1,20 +0,0 @@
export interface PSAData {
title: string;
description: string;
}
export interface PSA {
data: PSAData | null;
}
export const psa: PSA = { data: null };
console.log('Caching PSA...');
fetch(
'https://gist.githubusercontent.com/tresabhi/ed4b136f08856e615212e573aabe1968/raw',
)
.then((response) => response.json())
.then((data) => {
psa.data = data as PSAData;
console.log('PSA cached');
});
-16
View File
@@ -1,16 +0,0 @@
import { ChatInputCommandInteraction } from 'discord.js';
import primaryButton from './primaryButton';
export function refreshButton(
interaction: ChatInputCommandInteraction,
path: string,
) {
if (
interaction.appPermissions?.has('AttachFiles') &&
interaction.appPermissions?.has('ViewChannel')
) {
return primaryButton(path, 'Refresh');
}
return null;
}
@@ -1,27 +0,0 @@
import { ChatInputCommandInteraction } from 'discord.js';
import { Region } from '../../constants/regions';
import listClans from '../blitz/listClans';
import throwError from '../node/throwError';
import { serverAndIdPattern } from './resolvePlayerFromCommand';
export default async function resolveClanFromCommand(
interaction: ChatInputCommandInteraction,
) {
const clan = interaction.options.getString('clan', true);
if (serverAndIdPattern.test(clan)) {
const [server, accountId] = clan.split('/');
return { region: server as Region, id: Number(accountId) };
} else {
const accounts = await listClans(clan);
if (accounts[0]) {
return { region: accounts[0].region, id: accounts[0].clan_id };
} else {
throw throwError(
'Could not find clan',
`I couldn't find clan \`${clan}\`. Try selecting a username from the search result.`,
);
}
}
}
@@ -1,43 +0,0 @@
import { ButtonInteraction } from 'discord.js';
import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzstars/getPeriodNow';
import getPeriodStart from '../blitzstars/getPeriodStart';
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
import { PeriodType } from './addPeriodSubCommands';
import {
ResolvedPeriod,
getPeriodOptionName,
} 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);
start = getPeriodStart(region, periodSubcommand);
end = getPeriodNow();
}
return {
name,
start,
end,
} satisfies ResolvedPeriod;
}
@@ -1,49 +0,0 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzstars/getPeriodNow';
import getPeriodStart from '../blitzstars/getPeriodStart';
import getTimeDaysAgo from '../blitzstars/getTimeDaysAgo';
import { PeriodSize, PeriodType } from '../discord/addPeriodSubCommands';
export function getPeriodOptionName(period: PeriodSize) {
if (period === 'career') return 'Career';
if (period === 'today') return 'Today';
return `${period} days`;
}
export interface ResolvedPeriod {
name: string;
start: number;
end: number;
}
export default function resolvePeriodFromCommand(
region: Region,
interaction: ChatInputCommandInteraction<CacheType>,
) {
let name: string;
let start: number;
let end: number;
const periodSubcommand = 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 = `${startDaysAgoMin} - ${endDaysAgoMax} days`;
start = getTimeDaysAgo(region, endDaysAgoMax);
end = getTimeDaysAgo(region, startDaysAgoMin);
} else {
name = getPeriodOptionName(periodSubcommand);
start = getPeriodStart(region, periodSubcommand);
end = getPeriodNow();
}
return {
name,
start,
end,
} satisfies ResolvedPeriod;
}
@@ -1,14 +0,0 @@
import { ButtonInteraction } from 'discord.js';
import { Region } from '../../constants/regions';
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;
}
@@ -1,57 +0,0 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import markdownEscape from 'markdown-escape';
import { Region } from '../../constants/regions';
import listPlayers from '../blitz/listPlayers';
import { getBlitzFromDiscord } from '../cockroackdb/discordBlitz';
import throwError from '../node/throwError';
export const serverAndIdPattern = /(com|eu|asia)\/[0-9]+/;
export interface ResolvedPlayer {
region: Region;
id: number;
}
export default async function resolvePlayerFromCommand(
interaction: ChatInputCommandInteraction<CacheType>,
) {
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 listPlayers(commandUsername);
if (accounts[0]) {
return {
region: accounts[0].region,
id: accounts[0].account_id,
} satisfies ResolvedPlayer;
} else {
throw throwError(
'Could not find user',
`I couldn't find user "${markdownEscape(
commandUsername,
)}". Try picking an user from the search result, typing in a valid username, or using the \`/link\` command.`,
);
}
}
} else {
const account = await getBlitzFromDiscord(parseInt(interaction.user.id));
if (account) {
return { region: account.region, id: account.blitz };
} else {
throw throwError(
"You're not linked",
'Use the `/link` command to link your Discord and Blitz accounts.',
);
}
}
}