feat: satori and resvg for rendering

This commit is contained in:
TresAbhi committed 2023-05-28 02:25:59 +00:00
1 parent e48ea4c307
commit c834f4f966
34 files changed
+670 -501

No files matched your search

Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,6 @@ import {
ButtonBuilder,
SlashCommandBuilder,
} from 'discord.js';
import escapeHTML from 'escape-html';
import GenericStats from '../components/GenericStats/index.js';
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
@@ -19,7 +18,7 @@ import getBlitzStarsAccount from '../core/blitzstars/getBlitzStarsAccount.js';
import cmdName from '../core/interaction/cmdName.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import { args } from '../core/process/args.js';
import screenshotHTML from '../core/ui/screenshotHTML.js';
import render from '../core/ui/render.js';
import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo, AllStats } from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.js';
@@ -118,22 +117,27 @@ export default {
}
if (!stats) return;
const screenshot = await screenshotHTML(
Wrapper(
TitleBar(
escapeHTML(blitzStarsAccount.nickname),
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : '',
clanData[id]?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
: undefined,
`${periodNames[period]} • ${new Date().toDateString()} • ${
BLITZ_SERVERS[server]
}`,
),
const image = await render(
<Wrapper>
<TitleBar
name={blitzStarsAccount.nickname}
nameDiscriminator={
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
}
image={
clanData[id]?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
: undefined
}
description={`${
periodNames[period]
} • ${new Date().toDateString()} • ${BLITZ_SERVERS[server]}`}
/>
stats.all.battles === 0
? NoBattlesInPeriod()
: GenericStats([
{stats.all.battles === 0 && <NoBattlesInPeriod />}
{stats.all.battles > 0 && (
<GenericStats
stats={[
[
'Winrate',
`${(100 * (stats.all.wins / stats.all.battles)).toFixed(2)}%`,
@@ -183,10 +187,12 @@ export default {
(stats.all.battles - stats.all.survived_battles)
).toFixed(2),
],
]),
]}
/>
)}
PoweredByBlitzStars(),
),
<PoweredByBlitzStars />
</Wrapper>,
);
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
@@ -195,7 +201,7 @@ export default {
);
await interaction.editReply({
files: [screenshot],
files: [image],
components: [actionRow],
});
@@ -3,8 +3,7 @@ import {
ButtonBuilder,
SlashCommandBuilder,
} from 'discord.js';
import escapeHTML from 'escape-html';
import Breakdown from '../components/Breakdown.js';
import * as Breakdown from '../components/Breakdown/index.js';
import NoBattlesInPeriod from '../components/NoBattlesInPeriod.js';
import PoweredByBlitzStars from '../components/PoweredByBlitzStars.js';
import TitleBar from '../components/TitleBar.js';
@@ -21,7 +20,7 @@ import { tankopedia } from '../core/blitzstars/tankopedia.js';
import cmdName from '../core/interaction/cmdName.js';
import addUsernameOption from '../core/options/addUsernameOption.js';
import { args } from '../core/process/args.js';
import screenshotHTML from '../core/ui/screenshotHTML.js';
import render from '../core/ui/render.js';
import { CommandRegistry } from '../events/interactionCreate.js';
import { AccountInfo, AllStats } from '../types/accountInfo.js';
import { PlayerClanData } from '../types/playerClanData.js';
@@ -30,7 +29,7 @@ import resolveTankName from '../utilities/resolveTankName.js';
export default {
inProduction: true,
inDevelopment: true,
inDevelopment: false,
inPublic: true,
command: new SlashCommandBuilder()
@@ -72,43 +71,51 @@ export default {
([tankId, tankStats]) => {
const career = careerStats[tankId as unknown as number];
return Breakdown.Row(
resolveTankName({
tank_id: tankId as unknown as number,
name: tankopedia.data[tankId as unknown as number].name,
}),
tankStats.wins / tankStats.battles,
career.wins / career.battles,
-Infinity,
-Infinity,
tankStats.damage_dealt / tankStats.battles,
career.damage_dealt / career.battles,
tankStats.survived_battles / tankStats.battles,
career.survived_battles / career.battles,
tankStats.battles,
career.battles,
tankopedia.data[tankId as unknown as number].images.normal,
return (
<Breakdown.Row
key={tankId}
name={resolveTankName({
tank_id: tankId as unknown as number,
name: tankopedia.data[tankId as unknown as number].name,
})}
winrate={tankStats.wins / tankStats.battles}
careerWinrate={career.wins / career.battles}
WN8={-Infinity}
careerWN8={-Infinity}
damage={tankStats.damage_dealt / tankStats.battles}
careerDamage={career.damage_dealt / career.battles}
survival={tankStats.survived_battles / tankStats.battles}
careerSurvival={career.survived_battles / career.battles}
battles={tankStats.battles}
careerBattles={career.battles}
icon={tankopedia.data[tankId as unknown as number].images.normal}
/>
);
},
);
const screenshot = await screenshotHTML(
Wrapper(
TitleBar(
escapeHTML(accountInfo[id].nickname),
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : '',
clanData[id]?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
: undefined,
`Today's breakdown • ${new Date().toDateString()} • ${
const image = await render(
<Wrapper>
<TitleBar
name={accountInfo[id].nickname}
nameDiscriminator={
clanData[id]?.clan ? `[${clanData[id]?.clan?.tag}]` : undefined
}
image={
clanData[id]?.clan
? `https://wotblitz-gc.gcdn.co/icons/clanEmblems1x/clan-icon-v2-${clanData[id]?.clan?.emblem_set_id}.png`
: undefined
}
description={`Today's breakdown • ${new Date().toDateString()} • ${
BLITZ_SERVERS[server]
}`,
),
}`}
/>
rows.length === 0 ? NoBattlesInPeriod() : Breakdown.Root(...rows),
{rows.length === 0 && <NoBattlesInPeriod />}
{rows.length > 0 && <Breakdown.Root>{rows}</Breakdown.Root>}
PoweredByBlitzStars(),
),
<PoweredByBlitzStars />
</Wrapper>,
);
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
@@ -117,7 +124,7 @@ export default {
);
await interaction.editReply({
files: [screenshot],
files: [image],
components: [actionRow],
});
-183
View File
@@ -1,183 +0,0 @@
import Children from './Children.js';
const Breakdown = {
Root(...children: string[]) {
return `
<style>
.breakdown {
display: flex;
flex-direction: column;
gap: 16px;
}
</style>
<div class="breakdown">
${Children(children)}
</div>
`;
},
Row(
name: string,
winrate: number,
careerWinrate: number,
wn8: number,
careerWn8: number,
averageDamage: number,
careerAverageDamage: number,
survival: number,
careerSurvival: number,
battles: number,
careerBattles: number,
icon?: string,
) {
return `
<style>
.breakdown-row {
display: flex;
gap: 16px;
}
</style>
<div class="breakdown-row">
${this.RowDiscriminator(name, icon)}
${this.RowStat(
'Winrate',
`${(winrate * 100).toFixed(2)}%`,
`${(careerWinrate * 100).toFixed(2)}%`,
winrate - careerWinrate,
)}
${this.RowStat(
'Damage',
averageDamage.toFixed(2),
careerAverageDamage.toFixed(2),
averageDamage - careerAverageDamage,
)}
${this.RowStat(
'Survival',
`${(survival * 100).toFixed(2)}%`,
`${(careerSurvival * 100).toFixed(2)}%`,
survival - careerSurvival,
)}
${this.RowStat('Battles', battles.toFixed(0), careerBattles.toFixed(0))}
</div>
`;
/*
${this.RowStat(
'WN8',
wn8.toFixed(0),
careerWn8.toFixed(0),
wn8 - careerWn8,
)}
*/
},
RowDiscriminator(name: string, icon?: string) {
return `
<style>
.breakdown-row-discriminator {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
box-sizing: border-box;
width: 0px;
align-items: center;
justify-content: center;
}
.breakdown-row-discriminator-icon {
width: 100%;
flex: 1;
object-fit: cover;
}
.breakdown-row-discriminator-name {
color: #A0A0A0;
font-size: 16px;
text-align: center;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
</style>
<div class="breakdown-row-discriminator">
${
icon
? `<img src="${icon}" class="breakdown-row-discriminator-icon" />`
: ''
}
<span class="breakdown-row-discriminator-name">${name}</span>
</div>
`;
},
RowStat(name: string, value: string, career: string, delta?: number) {
return `
<style>
.breakdown-row-stat {
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
justify-content: center;
flex: 1;
}
.breakdown-row-stat-career {
color: #A0A0A0;
font-size: 12px;
}
.breakdown-row-stat-name {
color: #A0A0A0;
font-size: 16px;
}
.breakdown-row-stat-value {
display: flex;
gap: 4px;
align-items: center;
justify-content: center;
}
.breakdown-row-stat-value-delta {
width: 12px;
height: 12px;
}
.breakdown-row-stat-value-text {
color: white;
font-size: 16px;
font-weight: bold;
}
</style>
<div class="breakdown-row-stat">
<span class="breakdown-row-stat-career">${career}</span>
<div class="breakdown-row-stat-value">
${
delta
? `<img src="${
(delta ?? 0) > 0
? 'https://i.imgur.com/qbjiXa1.png'
: 'https://i.imgur.com/3uyNhun.png'
}" class="breakdown-row-stat-value-delta" />`
: ''
}
<span class="breakdown-row-stat-value-text">${value}</span>
</div>
<span class="breakdown-row-stat-name">${name}</span>
</div>
`;
},
};
export default Breakdown;
@@ -0,0 +1,13 @@
import { ReactNode } from 'react';
export interface RootProps {
children: ReactNode;
}
export function Root({ children }: RootProps) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
{children}
</div>
);
}
@@ -0,0 +1,61 @@
import { RowDiscriminator } from './RowDiscriminator.js';
import { RowStat } from './RowStat.js';
export interface RowProps {
name: string;
winrate: number;
careerWinrate: number;
WN8: number;
careerWN8: number;
damage: number;
careerDamage: number;
survival: number;
careerSurvival: number;
battles: number;
careerBattles: number;
icon?: string;
}
export function Row({
name,
winrate,
careerWinrate,
WN8,
careerWN8,
damage: averageDamage,
careerDamage: careerAverageDamage,
survival,
careerSurvival,
battles,
careerBattles,
icon,
}: RowProps) {
return (
<div style={{ display: 'flex', gap: 16 }}>
<RowDiscriminator name={name} icon={icon} />
<RowStat
name="Winrate"
value={`${(winrate * 100).toFixed(2)}%`}
career={`${(careerWinrate * 100).toFixed(2)}%`}
delta={winrate - careerWinrate}
/>
<RowStat
name="Damage"
value={averageDamage.toFixed(2)}
career={careerAverageDamage.toFixed(2)}
delta={averageDamage - careerAverageDamage}
/>
<RowStat
name="Survival"
value={`${(survival * 100).toFixed(2)}%`}
career={`${(careerSurvival * 100).toFixed(2)}%`}
delta={survival - careerSurvival}
/>
<RowStat
name="Battles"
value={battles.toFixed(0)}
career={careerBattles.toFixed(0)}
/>
</div>
);
}
@@ -0,0 +1,42 @@
export interface RowDiscriminatorProps {
name: string;
icon?: string;
}
export function RowDiscriminator({ name, icon }: RowDiscriminatorProps) {
return (
<div
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
height: '100%',
boxSizing: 'border-box',
width: 0,
alignItems: 'center',
justifyContent: 'center',
}}
>
{icon && (
<img
src={icon}
style={{ width: '100%', flex: 1, objectFit: 'cover' }}
/>
)}
<span
style={{
color: '#A0A0A0',
fontSize: 16,
textAlign: 'center',
display: 'block',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '100%',
}}
>
{name}
</span>
</div>
);
}
@@ -0,0 +1,49 @@
export interface RowStatProps {
name: string;
value: string;
career: string;
delta?: number;
}
export function RowStat({ name, value, career, delta }: RowStatProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
flex: 1,
}}
>
<span style={{ color: '#A0A0A0', fontSize: 12 }}>{career}</span>
<div
style={{
display: 'flex',
gap: 4,
alignItems: 'center',
justifyContent: 'center',
}}
>
{delta && (
<img
src={
(delta ?? 0) > 0
? 'https://i.imgur.com/qbjiXa1.png'
: 'https://i.imgur.com/3uyNhun.png'
}
style={{ width: 12, height: 12 }}
/>
)}
<span style={{ color: 'white', fontSize: 16, fontWeight: 'bold' }}>
{value}
</span>
</div>
<span style={{ color: '#A0A0A0', fontSize: 12 }}>{name}</span>
</div>
);
}
+4
View File
@@ -0,0 +1,4 @@
export * from './components/Root.js';
export * from './components/Row.js';
export * from './components/RowDiscriminator.js';
export * from './components/RowStat.js';
-5
View File
@@ -1,5 +0,0 @@
export type ChildrenArgument = string | string[];
export default function Children(children: string[]) {
return children.join('\n');
}
@@ -1,19 +0,0 @@
import { Stat } from '../index.js';
import GenericStatsRow from './GenericStatsRow.js';
export default function GenericStatsColumn(stats: Stat[]) {
return `
<style>
.generic-stats-column {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
</style>
<div class="generic-stats-column">
${stats.map((stat) => GenericStatsRow(stat)).join('\n')}
</div>
`;
}
@@ -0,0 +1,16 @@
import { Stat } from '../index.js';
import GenericStatsRow from './GenericStatsRow.js';
export interface GenericStatsColumnProps {
stats: Stat[];
}
export default function GenericStatsColumn({ stats }: GenericStatsColumnProps) {
return (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 8 }}>
{stats.map((stat) => (
<GenericStatsRow key={stat[0]} stat={stat} />
))}
</div>
);
}
@@ -1,29 +0,0 @@
import { Stat } from '../index.js';
export default function GenericStatsRow(stat: Stat) {
return `
<style>
.generic-stats-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.generic-stats-label {
color: #A0A0A0;
font-size: 16px;
}
.generic-stats-value {
color: white;
font-weight: bold;
font-size: 16px;
}
</style>
<div class="generic-stats-row">
<span class="generic-stats-label">${stat[0]}</span>
<span class="generic-stats-value">${stat[1]}</span>
</div>
`;
}
@@ -0,0 +1,22 @@
import { Stat } from '../index.js';
export interface GenericStatsRowProps {
stat: Stat;
}
export default function GenericStatsRow({ stat }: GenericStatsRowProps) {
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<span style={{ color: '#A0A0A0', fontSize: 16 }}>{stat[0]}</span>
<span style={{ color: 'white', fontWeight: 'bold', fontSize: 16 }}>
{stat[1]}
</span>
</div>
);
}
@@ -2,7 +2,15 @@ import GenericStatsColumn from './components/GenericStatsColumn.js';
export type Stat = [string, string | number];
export default function GenericStats(stats: Stat[], columnCount = 2) {
export interface GenericStatsProps {
stats: Stat[];
columnCount?: number;
}
export default function GenericStats({
stats,
columnCount = 2,
}: GenericStatsProps) {
const itemsPerRow = Math.ceil(stats.length / columnCount);
const columns: Stat[][] = [];
@@ -12,16 +20,14 @@ export default function GenericStats(stats: Stat[], columnCount = 2) {
columns[column].push(stat);
});
return `
<style>
.generic-stats {
display: flex;
gap: 32px;
}
</style>
<div class="generic-stats">
${columns.map((column) => GenericStatsColumn(column)).join('\n')}
return (
<div style={{ display: 'flex', gap: 32 }}>
{columns.map((stats) => (
<GenericStatsColumn
key={stats.map((stat) => stat[0]).join('-')}
stats={stats}
/>
))}
</div>
`;
);
}
-20
View File
@@ -1,20 +0,0 @@
export default function NoBattlesInPeriod() {
return `
<style>
.no-battles-in-period {
display: flex;
align-items: center;
justify-content: center;
}
.no-battles-in-period-text {
font-size: 16px;
color: #A0A0A0;
}
</style>
<div class="no-battles-in-period">
<span class="no-battles-in-period-text">No battles played in this period</span>
</div>
`;
}
+15
View File
@@ -0,0 +1,15 @@
export default function NoBattlesInPeriod() {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<span style={{ fontSize: 16, color: '#A0A0A0' }}>
No battles played in this period
</span>
</div>
);
}
-26
View File
@@ -1,26 +0,0 @@
export default function PoweredByBlitzStars() {
return `
<style>
.powered-by-blitz-stars {
display: flex;
gap: 4px;
justify-content: flex-end;
}
.powered-by-blitz-stars-icon {
height: 16px;
width: 16px;
}
.powered-by-blitz-stars-text {
font-size: 16px;
color: #A0A0A0;
}
</style>
<div class="powered-by-blitz-stars">
<img class="powered-by-blitz-stars-icon" src="https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png" />
<span class="powered-by-blitz-stars-text">Powered by BlitzStars</span>
</div>
`;
}
+13
View File
@@ -0,0 +1,13 @@
export default function PoweredByBlitzStars() {
return (
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
<img
src="https://www.blitzstars.com/assets/images/4b4cb591.TankyMcPewpew.png"
style={{ height: 16, width: 16 }}
/>
<span style={{ color: '#A0A0A0', fontSize: 16 }}>
Powered by BlitzStars
</span>
</div>
);
}
-26
View File
@@ -1,26 +0,0 @@
import Children from './Children.js';
export default function Root(...children: string[]) {
return `
<style>
@import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:[email protected]');
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:[email protected]');
body {
margin: 0;
}
* {
font-family: 'Roboto Flex', sans-serif;
}
#root {
display: flex;
}
</style>
<div id="root">
${Children(children)}
</div>
`;
}
-69
View File
@@ -1,69 +0,0 @@
export default function TitleBar(
name: string,
nameDiscriminator: string,
image?: string,
description?: string,
) {
return `
<style>
.title-bar {
display: flex;
justify-content: space-between;
align-items: center;
}
.title-bar-info {
display: flex;
gap: 8px;
}
.title-bar-avatar {
width: 64px;
height: 64px;
}
.title-bar-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.title-bar-name {
font-size: 32px;
color: white;
font-weight: 900;
}
.title-bar-discriminator {
color: #A0A0A0;
}
.title-bar-description {
color: #A0A0A0;
font-size: 16px;
}
.title-bar-blitzkrieg {
width: 64px;
height: 64px;
}
</style>
<div class="title-bar">
<div class="title-bar-info">
${image ? `<img class="title-bar-avatar" src="${image}" />` : ''}
<div class="title-bar-text">
<span class="title-bar-name">
${name}
<span class="title-bar-discriminator">${nameDiscriminator}</span>
</span>
<span class="title-bar-description">${description}</span>
</div>
</div>
<img class="title-bar-blitzkrieg" src="https://i.imgur.com/35phKIu.png" />
</div>
`;
}
+50
View File
@@ -0,0 +1,50 @@
export interface TitleBarProps {
name: string;
nameDiscriminator?: string;
image?: string;
description?: string;
}
export default function TitleBar({
name,
nameDiscriminator,
image,
description,
}: TitleBarProps) {
return (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div style={{ display: 'flex', gap: 8 }}>
{image && <img style={{ width: 64, height: 64 }} src={image} />}
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<span
style={{
fontSize: 32,
color: 'white',
fontWeight: 900,
gap: 4,
}}
>
{name}
{nameDiscriminator && (
<span style={{ color: '#A0A0A0' }}>{nameDiscriminator}</span>
)}
</span>
<span style={{ color: '#A0A0A0', fontSize: 16 }}>{description}</span>
</div>
</div>
<img
style={{ width: 64, height: 64 }}
src="https://i.imgur.com/35phKIu.png"
/>
</div>
);
}
-21
View File
@@ -1,21 +0,0 @@
import Children from './Children.js';
export default function Wrapper(...children: string[]) {
return `
<style>
#wrapper {
display: flex;
flex-direction: column;
padding: 16px;
gap: 32px;
width: 640px;
background: black;
color: white;
}
</style>
<div id="wrapper">
${Children(children)}
</div>
`;
}
+23
View File
@@ -0,0 +1,23 @@
import { ReactNode } from 'react';
export interface WrapperProps {
children: ReactNode;
}
export default function Wrapper({ children }: WrapperProps) {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
padding: 16,
gap: 32,
width: 640,
background: 'black',
color: 'white',
}}
>
{children}
</div>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { Resvg } from '@resvg/resvg-js';
import { readFileSync } from 'fs';
import { ReactNode } from 'react';
import satori from 'satori';
const robotoFlex = readFileSync('src/assets/fonts/Roboto.ttf');
const robotoFlexBold = readFileSync('src/assets/fonts/Roboto-Bold.ttf');
const robotoFlexBlack = readFileSync('src/assets/fonts/Roboto-Black.ttf');
export default async function render(element: ReactNode) {
const svg = await satori(element, {
width: 640,
fonts: [
{ data: robotoFlex, name: 'Roboto Flex', weight: 400 },
{ data: robotoFlexBold, name: 'Roboto Flex', weight: 700 },
{ data: robotoFlexBlack, name: 'Roboto Flex', weight: 900 },
],
});
const png = new Resvg(svg).render().asPng();
return png;
}
-14
View File
@@ -1,14 +0,0 @@
import Root from '../../components/Root.js';
import { browser } from '../../index.js';
export default async function screenshotHTML(children: string) {
const page = await browser.newPage();
await page.setViewportSize({ width: 640, height: 1 });
await page.setContent(Root(children));
const screenshot = await page
.locator('#root')
.screenshot({ omitBackground: true, scale: 'css' });
await page.close();
return screenshot;
}
-3
View File
@@ -1,4 +1,3 @@
import { chromium } from '@playwright/test';
import { Client, GatewayIntentBits } from 'discord.js';
import { args } from './core/process/args.js';
import { registerErrorHandlers } from './events/error.js';
@@ -17,5 +16,3 @@ client
.on('guildMemberAdd', guildMemberAdd)
.on('interactionCreate', interactionCreate)
.login(args['discord-token']);
export const browser = await chromium.launch();
View File
File renamed without changes.