feat: core chunk 7

This commit is contained in:
TrèsAbhi committed 2023-10-14 11:36:10 -05:00
1 parent d726005c38
commit 01dd7ad9c6
39 files changed
+83 -87

No files matched your search

@@ -1,8 +1,8 @@
import { octokit } from '../../LEGACY_core/github/octokit';
import throwError from '../../LEGACY_core/node/throwError';
import { RatingsInfo } from '../../commands/ratings';
import { Region } from '../../constants/regions';
import { DATABASE_REPO } from './getMidnightLeaderboard';
import { octokit } from './octokit';
const ARCHIVED_RATINGS_CACHE: Record<
Region,
+6
View File
@@ -0,0 +1,6 @@
import discord from '../../../discord.json' assert { type: 'json' };
import isDev from './isDev';
export default function getClientId() {
return isDev() ? discord.client_id_dev : discord.client_id;
}
@@ -1,6 +1,6 @@
import { BlitzkriegRatingsLeaderboard } from '../../../scripts/buildRatingsLeaderboard';
import { Region } from '../../constants/regions';
import { octokit } from '../../LEGACY_core/github/octokit';
import { octokit } from './octokit';
export const DATABASE_REPO = { owner: 'tresabhi', repo: 'blitzkrieg-db' };
+3
View File
@@ -0,0 +1,3 @@
export default function isDev() {
return process.env.NODE_ENV === 'development';
}
+9
View File
@@ -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;
}
+42
View File
@@ -0,0 +1,42 @@
import { readFile } from 'fs';
import { join } from 'path';
import satori from 'satori';
import robotoBlackFile from '../../assets/fonts/Roboto-Black.ttf';
import robotoBoldFile from '../../assets/fonts/Roboto-Bold.ttf';
import robotoFile from '../../assets/fonts/Roboto.ttf';
const FONT_NAME = 'Roboto';
const FONT_FILES = [robotoFile, robotoBoldFile, robotoBlackFile];
let roboto: Buffer;
let robotoBold: Buffer;
let robotoBlack: Buffer;
console.log('Importing fonts...');
Promise.all(
FONT_FILES.map(
(file) =>
new Promise<Buffer>((resolve, reject) => {
const path = join(__dirname, file);
readFile(path, (error, data) => {
if (error) reject(error);
resolve(data);
});
}),
),
).then((data) => {
[roboto, robotoBold, robotoBlack] = data;
console.log('Fonts imported');
});
export default async function jsxToSvg(jsx: JSX.Element) {
return await satori(jsx, {
width: 480,
fonts: [
{ data: roboto, name: FONT_NAME, weight: 400 },
{ data: robotoBold, name: FONT_NAME, weight: 700 },
{ data: robotoBlack, name: FONT_NAME, weight: 900 },
],
});
}
+4
View File
@@ -0,0 +1,4 @@
import { Octokit } from '@octokit/rest';
import { secrets } from '../../LEGACY_core/node/secrets';
export const octokit = new Octokit({ auth: secrets.GH_TOKEN });
+5
View File
@@ -0,0 +1,5 @@
import { Resvg } from '@resvg/resvg-js';
export default function svgToPng(svg: string) {
return new Resvg(svg).render().asPng();
}
@@ -1,6 +1,6 @@
import linkButton from '../../LEGACY_core/discord/linkButton';
import { Region } from '../../constants/regions';
import { getAccountInfo } from '../blitz/getAccountInfo';
import buttonLink from '../discord/buttonLink';
export async function getBlitzStarsLinkButton(
region: Region,
@@ -9,7 +9,7 @@ export async function getBlitzStarsLinkButton(
) {
const { nickname } = await getAccountInfo(region, id);
return linkButton(
return buttonLink(
`https://www.blitzstars.com/player/${region}/${nickname}${
tankId ? `/tank/${tankId!}` : ''
}`,
+20
View File
@@ -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;
}
+20
View File
@@ -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;
}
+16
View File
@@ -0,0 +1,16 @@
import { ChatInputCommandInteraction } from 'discord.js';
import buttonPrimary from './buttonPrimary';
export function buttonRefresh(
interaction: ChatInputCommandInteraction,
path: string,
) {
if (
interaction.appPermissions?.has('AttachFiles') &&
interaction.appPermissions?.has('ViewChannel')
) {
return buttonPrimary(path, 'Refresh');
}
return null;
}
+3
View File
@@ -4,6 +4,9 @@ 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,
+9
View File
@@ -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,48 @@
import {
ActionRowBuilder,
AttachmentBuilder,
ButtonBuilder,
EmbedBuilder,
InteractionEditReplyOptions,
} from 'discord.js';
import { InteractionReturnable } from '../../events/interactionCreate';
import jsxToPng from '../blitzkrieg/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
@@ -0,0 +1,20 @@
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');
});
@@ -0,0 +1,27 @@
import { ChatInputCommandInteraction } from 'discord.js';
import throwError from '../../LEGACY_core/node/throwError';
import { Region } from '../../constants/regions';
import searchClansAcrossRegions from '../blitz/searchClansAcrossRegions';
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 searchClansAcrossRegions(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.`,
);
}
}
}
@@ -0,0 +1,43 @@
import { ButtonInteraction } from 'discord.js';
import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzkrieg/getPeriodNow';
import getPeriodStart from '../blitzkrieg/getPeriodStart';
import getTimeDaysAgo from '../blitzkrieg/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;
}
@@ -0,0 +1,49 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import { Region } from '../../constants/regions';
import getPeriodNow from '../blitzkrieg/getPeriodNow';
import getPeriodStart from '../blitzkrieg/getPeriodStart';
import getTimeDaysAgo from '../blitzkrieg/getTimeDaysAgo';
import { PeriodSize, PeriodType } from './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;
}
@@ -0,0 +1,14 @@
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;
}
@@ -0,0 +1,57 @@
import { CacheType, ChatInputCommandInteraction } from 'discord.js';
import markdownEscape from 'markdown-escape';
import throwError from '../../LEGACY_core/node/throwError';
import { Region } from '../../constants/regions';
import searchPlayersAcrossRegions from '../blitz/searchPlayersAcrossRegions';
import { getBlitzFromDiscord } from '../blitzkrieg/discordBlitz';
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 searchPlayersAcrossRegions(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.',
);
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export default function isNumber(value: any) {
return typeof value === 'number' && isFinite(value);
}