aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-10-03 22:57:40 -0400
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-10-03 22:57:40 -0400
commit612ed820a0600ec11ed642005377cd7f5a8a8b77 (patch)
tree6bca4e7268fd0063ff53cf64fa44df62a23dba50 /src
parented98ff7e2679f362f2657e77a6cf8dd3ce9b3d43 (diff)
downloadtanzanite-612ed820a0600ec11ed642005377cd7f5a8a8b77.tar.gz
tanzanite-612ed820a0600ec11ed642005377cd7f5a8a8b77.tar.bz2
tanzanite-612ed820a0600ec11ed642005377cd7f5a8a8b77.zip
wip
Diffstat (limited to 'src')
-rw-r--r--src/bot.ts28
-rw-r--r--src/commands/config/config.ts17
-rw-r--r--src/commands/config/disable.ts4
-rw-r--r--src/commands/config/log.ts10
-rw-r--r--src/commands/dev/eval.ts2
-rw-r--r--src/commands/dev/superUser.ts2
-rw-r--r--src/commands/dev/test.ts156
-rw-r--r--src/commands/fun/minesweeper.ts4
-rw-r--r--src/commands/info/guildInfo.ts111
-rw-r--r--src/commands/info/help.ts4
-rw-r--r--src/commands/info/inviteInfo.ts7
-rw-r--r--src/commands/info/ping.ts2
-rw-r--r--src/commands/info/snowflake.ts48
-rw-r--r--src/commands/info/userInfo.ts112
-rw-r--r--src/commands/leveling/level.ts8
-rw-r--r--src/commands/leveling/setLevel.ts28
-rw-r--r--src/commands/leveling/setXp.ts34
-rw-r--r--src/commands/moderation/ban.ts4
-rw-r--r--src/commands/moderation/block.ts2
-rw-r--r--src/commands/moderation/evidence.ts2
-rw-r--r--src/commands/moderation/kick.ts2
-rw-r--r--src/commands/moderation/modlog.ts60
-rw-r--r--src/commands/moderation/mute.ts2
-rw-r--r--src/commands/moderation/myLogs.ts12
-rw-r--r--src/commands/moderation/role.ts2
-rw-r--r--src/commands/moderation/slowmode.ts6
-rw-r--r--src/commands/moderation/timeout.ts8
-rw-r--r--src/commands/moderation/unblock.ts8
-rw-r--r--src/commands/moderation/unmute.ts8
-rw-r--r--src/commands/moderation/untimeout.ts8
-rw-r--r--src/commands/moderation/warn.ts2
-rw-r--r--src/commands/moulberry-bush/capes.ts6
-rw-r--r--src/commands/tickets/ticket-!.ts2
-rw-r--r--src/commands/utilities/activity.ts2
-rw-r--r--src/commands/utilities/highlight-!.ts2
-rw-r--r--src/commands/utilities/highlight-block.ts2
-rw-r--r--src/commands/utilities/highlight-matches.ts2
-rw-r--r--src/commands/utilities/highlight-unblock.ts2
-rw-r--r--src/commands/utilities/price.ts4
-rw-r--r--src/commands/utilities/steal.ts8
-rw-r--r--src/commands/utilities/whoHasRole.ts2
-rw-r--r--src/commands/utilities/wolframAlpha.ts6
-rw-r--r--src/context-menu-commands/message/viewRaw.ts2
-rw-r--r--src/context-menu-commands/user/modlog.ts9
-rw-r--r--src/context-menu-commands/user/userInfo.ts28
-rw-r--r--src/listeners/bush/appealListener.ts14
-rw-r--r--src/listeners/bush/experimentYoink.ts28
-rw-r--r--src/listeners/client/ready.ts28
-rw-r--r--src/listeners/commands/commandBlocked.ts4
-rw-r--r--src/listeners/contextCommands/contextCommandBlocked.ts4
-rw-r--r--src/listeners/contextCommands/contextCommandError.ts2
-rw-r--r--src/listeners/contextCommands/contextCommandNotFound.ts2
-rw-r--r--src/listeners/contextCommands/contextCommandStarted.ts2
-rw-r--r--src/listeners/guild/syncUnbanPunishmentModel.ts2
-rw-r--r--src/listeners/interaction/$interactionCreate.ts31
-rw-r--r--src/listeners/interaction/button.ts129
-rw-r--r--src/listeners/interaction/interactionCreate.ts78
-rw-r--r--src/listeners/interaction/modalSubmit.ts20
-rw-r--r--src/listeners/interaction/selectMenu.ts23
-rw-r--r--src/listeners/member-custom/levelUpdate.ts3
-rw-r--r--src/listeners/message/autoPublisher.ts5
-rw-r--r--src/listeners/message/level.ts57
-rw-r--r--src/listeners/message/quoteCreate.ts2
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncBan.ts2
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncKick.ts2
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncTimeout.ts2
-rw-r--r--src/listeners/track-manual-punishments/modlogSyncUnban.ts2
-rw-r--r--src/listeners/ws/INTERACTION_CREATE.ts236
-rw-r--r--src/tasks/feature/removeExpiredPunishements.ts8
69 files changed, 779 insertions, 687 deletions
diff --git a/src/bot.ts b/src/bot.ts
index 486eb2e..99d3ee3 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -1,26 +1,38 @@
+import { performance } from 'node:perf_hooks';
+performance.mark('processStart');
+
console.log('Tanzanite is Starting');
-import { init } from '../lib/utils/Logger.js';
+import { init } from '#lib/utils/Logger.js';
// creates proxies on console.log and console.warn
// also starts a REPL session
init();
import { config } from '#config';
-import { dirname } from 'path';
-import { fileURLToPath } from 'url';
-import { Sentry } from '../lib/common/Sentry.js';
-import { TanzaniteClient } from '../lib/extensions/discord-akairo/TanzaniteClient.js';
+import { Sentry } from '#lib/common/Sentry.js';
+import { TanzaniteClient } from '#lib/extensions/discord-akairo/TanzaniteClient.js';
+import { dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
const isDry = process.argv.includes('dry');
-if (!isDry && config.credentials.sentryDsn !== null) new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd(), config);
+
+if (!isDry && config.credentials.sentryDsn !== null) {
+ new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd(), config);
+}
+
TanzaniteClient.extendStructures();
+
const client = new TanzaniteClient(config);
-// @ts-ignore: for debugging purposes
+// @ts-ignore: I don't want to add this to the global typings, this is only for debugging purposes
global.client = client;
-if (!isDry) await client.dbPreInit();
+if (!isDry) {
+ await client.dbPreInit();
+}
+
await client.init();
+
if (isDry) {
process.exit(0);
} else {
diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts
index adc41d8..4d38c54 100644
--- a/src/commands/config/config.ts
+++ b/src/commands/config/config.ts
@@ -13,8 +13,8 @@ import {
type GuildSettingType,
type SlashMessage
} from '#lib';
+import { ExtSub, type ArgumentGeneratorReturn, type SlashOption } from '@notenoughupdates/discord-akairo';
import assert from 'assert/strict';
-import { type ArgumentGeneratorReturn, type SlashOption } from 'discord-akairo';
import {
ActionRowBuilder,
ApplicationCommandOptionType,
@@ -32,10 +32,9 @@ import {
User,
type Message,
type MessageComponentInteraction,
- type MessageOptions
+ type MessageCreateOptions
} from 'discord.js';
-import _ from 'lodash';
-const { camelCase, snakeCase } = _;
+import { camelCase, snakeCase } from 'lodash-es';
export const arrayActions = ['view' as const, 'add' as const, 'remove' as const, 'clear' as const];
export type ArrayActions = typeof arrayActions[number];
@@ -79,7 +78,7 @@ export default class ConfigCommand extends BotCommand {
description: `Manage the server's ${loweredName}`,
type: ApplicationCommandOptionType.SubcommandGroup,
options: isArray
- ? [
+ ? ([
{
name: 'view',
description: `View the server's ${loweredName}.`,
@@ -118,8 +117,8 @@ export default class ConfigCommand extends BotCommand {
description: `Remove all values from a server's ${loweredName}.`,
type: ApplicationCommandOptionType.Subcommand
}
- ]
- : [
+ ] as ExtSub[])
+ : ([
{
name: 'view',
description: `View the server's ${loweredName}.`,
@@ -144,7 +143,7 @@ export default class ConfigCommand extends BotCommand {
description: `Delete the server's ${loweredName}.`,
type: ApplicationCommandOptionType.Subcommand
}
- ]
+ ] as ExtSub[])
};
}),
channel: 'guild',
@@ -309,7 +308,7 @@ export default class ConfigCommand extends BotCommand {
public async generateMessageOptions(
message: CommandMessage | SlashMessage,
setting?: undefined | keyof typeof guildSettingsObj
- ): Promise<MessageOptions & InteractionUpdateOptions> {
+ ): Promise<MessageCreateOptions & InteractionUpdateOptions> {
assert(message.inGuild());
const settingsEmbed = new EmbedBuilder().setColor(colors.default);
diff --git a/src/commands/config/disable.ts b/src/commands/config/disable.ts
index 6dd94a6..776ecf0 100644
--- a/src/commands/config/disable.ts
+++ b/src/commands/config/disable.ts
@@ -10,9 +10,9 @@ import {
} from '#lib';
import assert from 'assert/strict';
import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js';
-import { default as Fuse } from 'fuse.js';
-assert(Fuse);
+// todo: remove this bullshit once typescript gets its shit together
+const Fuse = (await import('fuse.js')).default as unknown as typeof import('fuse.js').default;
export default class DisableCommand extends BotCommand {
private static blacklistedCommands = ['eval', 'disable'];
diff --git a/src/commands/config/log.ts b/src/commands/config/log.ts
index 0c74ce7..6d0f594 100644
--- a/src/commands/config/log.ts
+++ b/src/commands/config/log.ts
@@ -8,8 +8,8 @@ import {
type GuildLogType,
type SlashMessage
} from '#lib';
+import { ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo';
import assert from 'assert/strict';
-import { ArgumentGeneratorReturn } from 'discord-akairo';
import { ApplicationCommandOptionType, ChannelType } from 'discord.js';
export default class LogCommand extends BotCommand {
@@ -38,10 +38,10 @@ export default class LogCommand extends BotCommand {
slashType: ApplicationCommandOptionType.Channel,
channelTypes: [
ChannelType.GuildText,
- ChannelType.GuildNews,
- ChannelType.GuildNewsThread,
- ChannelType.GuildPublicThread,
- ChannelType.GuildPrivateThread
+ ChannelType.GuildAnnouncement,
+ ChannelType.AnnouncementThread,
+ ChannelType.PublicThread,
+ ChannelType.PrivateThread
],
only: 'slash'
}
diff --git a/src/commands/dev/eval.ts b/src/commands/dev/eval.ts
index 83168e0..5fe21c0 100644
--- a/src/commands/dev/eval.ts
+++ b/src/commands/dev/eval.ts
@@ -283,7 +283,7 @@ export default class EvalCommand extends BotCommand {
if (!err && proto) embed.addFields({ name: ':gear: Proto', value: proto });
if (!silent || message.util.isSlashMessage(message)) {
- await message.util.reply({ content: null, embeds: [embed] });
+ await message.util.reply({ content: '', embeds: [embed] });
} else {
const success = await message.author.send({ embeds: [embed] }).catch(() => false);
if (!deleteMsg) await message.react(success ? emojis.successFull : emojis.errorFull).catch(() => {});
diff --git a/src/commands/dev/superUser.ts b/src/commands/dev/superUser.ts
index fc7fcbf..54332d6 100644
--- a/src/commands/dev/superUser.ts
+++ b/src/commands/dev/superUser.ts
@@ -1,5 +1,5 @@
import { BotCommand, emojis, format, type ArgType, type CommandMessage } from '#lib';
-import { type ArgumentGeneratorReturn, type ArgumentTypeCasterReturn } from 'discord-akairo';
+import { type ArgumentGeneratorReturn, type ArgumentTypeCasterReturn } from '@notenoughupdates/discord-akairo';
export default class SuperUserCommand extends BotCommand {
public constructor() {
diff --git a/src/commands/dev/test.ts b/src/commands/dev/test.ts
index e1f3b73..994b76f 100644
--- a/src/commands/dev/test.ts
+++ b/src/commands/dev/test.ts
@@ -1,13 +1,14 @@
-import { BotCommand, ButtonPaginator, colors, emojis, OptArgType, Shared, type CommandMessage } from '#lib';
+import { BotCommand, ButtonPaginator, chunk, colors, emojis, OptArgType, Shared, type CommandMessage } from '#lib';
import {
ActionRowBuilder,
+ APIEmbed,
ButtonBuilder,
ButtonStyle,
+ Collection,
EmbedBuilder,
- GatewayDispatchEvents,
+ Message,
Routes,
- type ApplicationCommand,
- type Collection
+ type ApplicationCommand
} from 'discord.js';
import badLinksSecretArray from '../../../lib/badlinks-secret.js';
import badLinksArray from '../../../lib/badlinks.js';
@@ -52,15 +53,38 @@ export default class TestCommand extends BotCommand {
return await message.util.reply(responses[Math.floor(Math.random() * responses.length)]);
}
+ console.dir(args);
+
if (args.feature) {
if (['button', 'buttons'].includes(args.feature?.toLowerCase())) {
const buttonRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
- new ButtonBuilder({ style: ButtonStyle.Primary, customId: 'primaryButton', label: 'Primary' }),
- new ButtonBuilder({ style: ButtonStyle.Secondary, customId: 'secondaryButton', label: 'Secondary' }),
- new ButtonBuilder({ style: ButtonStyle.Success, customId: 'successButton', label: 'Success' }),
- new ButtonBuilder({ style: ButtonStyle.Danger, customId: 'dangerButton', label: 'Danger' }),
- new ButtonBuilder({ style: ButtonStyle.Link, label: 'Link', url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' })
+ new ButtonBuilder({
+ style: ButtonStyle.Primary,
+ customId: 'test;button;primary',
+ label: 'Primary'
+ }),
+ new ButtonBuilder({
+ style: ButtonStyle.Secondary,
+ customId: 'test;button;secondary',
+ label: 'Secondary'
+ }),
+ new ButtonBuilder({
+ style: ButtonStyle.Success,
+ customId: 'test;button;success',
+ label: 'Success'
+ }),
+ new ButtonBuilder({
+ style: ButtonStyle.Danger,
+ customId: 'test;button;danger',
+ label: 'Danger'
+ }),
+ new ButtonBuilder({
+ style: ButtonStyle.Link,
+ label: 'Link',
+ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
+ })
);
+
return await message.util.reply({ content: 'buttons', components: [buttonRow] });
} else if (['embed', 'button embed'].includes(args.feature?.toLowerCase())) {
const embed = new EmbedBuilder()
@@ -80,18 +104,23 @@ export default class TestCommand extends BotCommand {
const buttonRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder({ style: ButtonStyle.Link, label: 'Link', url: 'https://google.com/' })
);
+
return await message.util.reply({ content: 'Test', embeds: [embed], components: [buttonRow] });
} else if (['lots of buttons'].includes(args.feature?.toLowerCase())) {
const buttonRows: ActionRowBuilder<ButtonBuilder>[] = [];
+
for (let a = 1; a <= 5; a++) {
const row = new ActionRowBuilder<ButtonBuilder>();
+
for (let b = 1; b <= 5; b++) {
- const id = (a + 5 * (b - 1)).toString();
+ const id = `test;lots;${a + 5 * (b - 1)}`;
const button = new ButtonBuilder({ style: ButtonStyle.Primary, customId: id, label: id });
row.addComponents(button);
}
+
buttonRows.push(row);
}
+
return await message.util.reply({ content: 'buttons', components: buttonRows });
} else if (['paginate'].includes(args.feature?.toLowerCase())) {
const embeds = [];
@@ -142,13 +171,16 @@ export default class TestCommand extends BotCommand {
return message.util.reply(`${emojis.error} no`);
} else if (['sync automod'].includes(args.feature?.toLowerCase())) {
const row = (await Shared.findByPk(0))!;
+
row.badLinks = badLinksArray;
row.badLinksSecret = badLinksSecretArray;
row.badWords = badWords;
+
await row.save();
+
return await message.util.reply(`${emojis.success} Synced automod.`);
} else if (['modal'].includes(args.feature?.toLowerCase())) {
- const m = await message.util.reply({
+ return await message.util.reply({
content: 'Click for modal',
components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(
@@ -156,63 +188,57 @@ export default class TestCommand extends BotCommand {
)
]
});
+ } else if (args.feature.includes('backlog experiments')) {
+ this.client.logger.debug('backlog experiments');
+
+ if (message.channelId !== '1019830755658055691') {
+ return await message.util.reply(`${emojis.error} This only works in <#1019830755658055691>.`);
+ }
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- this.client.ws.on(GatewayDispatchEvents.InteractionCreate, async (i: any) => {
- if (i?.data?.custom_id !== 'test;modal' || i?.data?.component_type !== 2) return;
- if (i?.message?.id !== m.id) return;
-
- const text = { type: 4, style: 1, min_length: 1, max_length: 4000, required: true };
-
- await this.client.rest.post(Routes.interactionCallback(i.id, i.token), {
- body: {
- type: 9,
- data: {
- custom_id: 'test;login',
- title: 'Login (real)',
- components: [
- {
- type: 1,
- components: [
- {
- ...text,
- custom_id: 'test;login;email',
- label: 'Email',
- placeholder: 'Email'
- }
- ]
- },
- {
- type: 1,
- components: [
- {
- ...text,
- custom_id: 'test;login;password',
- label: 'Password',
- placeholder: 'Password'
- }
- ]
- },
- {
- type: 1,
- components: [
- {
- ...text,
- custom_id: 'test;login;2fa',
- label: 'Enter Discord Auth Code',
- min_length: 6,
- max_length: 6,
- placeholder: '6-digit authentication code'
- }
- ]
- }
- ]
- }
- }
+ let messages = new Collection<string, Message>();
+ let lastID: string | undefined;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const fetchedMessages = await message.channel.messages.fetch({
+ limit: 100,
+ ...(lastID && { before: lastID })
});
- });
- return;
+ if (fetchedMessages.size === 0) {
+ break;
+ }
+
+ messages = messages.concat(fetchedMessages);
+ lastID = fetchedMessages.lastKey();
+
+ this.client.logger.debug(messages.size);
+ this.client.logger.debug(lastID);
+ }
+
+ const embeds = messages
+ .sort((a, b) => a.createdTimestamp - b.createdTimestamp)
+ .filter((m) => m.embeds.length > 0 && (m.embeds[0].title?.includes('Guild Experiment') ?? false))
+ .map(
+ (m): APIEmbed => ({
+ ...m.embeds[0]!.toJSON(),
+ timestamp: new Date(m.createdTimestamp).toISOString()
+ })
+ );
+
+ const chunked = chunk(embeds, 10);
+
+ let i = 0;
+ for (const chunk of chunked) {
+ this.client.logger.debug(i);
+ this.client.logger.debug(chunk, 1);
+ await this.client.rest.post(Routes.channelMessages('795356494261911553'), {
+ body: { embeds: chunk }
+ });
+ i++;
+ }
+
+ return await message.util.reply(`${emojis.success} Done.`);
}
}
return await message.util.reply(responses[Math.floor(Math.random() * responses.length)]);
diff --git a/src/commands/fun/minesweeper.ts b/src/commands/fun/minesweeper.ts
index 85945c7..b0528ac 100644
--- a/src/commands/fun/minesweeper.ts
+++ b/src/commands/fun/minesweeper.ts
@@ -1,8 +1,6 @@
import { BotCommand, emojis, OptArgType, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
-import { Minesweeper } from '@notenoughupdates/discord.js-minesweeper';
-import assert from 'assert/strict';
+import { Minesweeper } from '@tanzanite/discord.js-minesweeper';
import { ApplicationCommandOptionType } from 'discord.js';
-assert(Minesweeper);
export default class MinesweeperCommand extends BotCommand {
public constructor() {
diff --git a/src/commands/info/guildInfo.ts b/src/commands/info/guildInfo.ts
index e364a89..acd5b86 100644
--- a/src/commands/info/guildInfo.ts
+++ b/src/commands/info/guildInfo.ts
@@ -11,6 +11,7 @@ import {
type OptArgType,
type SlashMessage
} from '#lib';
+import { embedField } from '#lib/common/tags.js';
import assert from 'assert/strict';
import {
ApplicationCommandOptionType,
@@ -26,7 +27,6 @@ import {
PermissionFlagsBits,
type BaseGuildVoiceChannel,
type GuildPreview,
- type Snowflake,
type Vanity
} from 'discord.js';
@@ -66,9 +66,13 @@ export default class GuildInfoCommand extends BotCommand {
let guild: ArgType<'guild' | 'snowflake'> | GuildPreview = args.guild ?? message.guild!;
if (typeof guild === 'string') {
- const preview = await this.client.fetchGuildPreview(`${args.guild}` as Snowflake).catch(() => undefined);
- if (preview) guild = preview;
- else return await message.util.reply(`${emojis.error} That guild is not discoverable or does not exist.`);
+ const preview = await this.client.fetchGuildPreview(`${args.guild}`).catch(() => {});
+
+ if (preview) {
+ guild = preview;
+ } else {
+ return await message.util.reply(`${emojis.error} That guild is not discoverable or does not exist.`);
+ }
}
assert(guild);
@@ -96,10 +100,13 @@ export default class GuildInfoCommand extends BotCommand {
const otherEmojis = mappings.otherEmojis;
const verifiedGuilds = Object.values(mappings.guilds);
- if (verifiedGuilds.includes(guild.id as typeof verifiedGuilds[number])) description.push(otherEmojis.BushVerified);
- if (guild instanceof Guild) {
- if (guild.premiumTier !== GuildPremiumTier.None) description.push(otherEmojis[`BoostTier${guild.premiumTier}`]);
+ if (verifiedGuilds.includes(guild.id as typeof verifiedGuilds[number])) {
+ description.push(otherEmojis.BushVerified);
+ }
+
+ if (guild instanceof Guild && guild.premiumTier !== GuildPremiumTier.None) {
+ description.push(otherEmojis[`BoostTier${guild.premiumTier}`]);
}
const features = mappings.features;
@@ -138,52 +145,65 @@ export default class GuildInfoCommand extends BotCommand {
)
] as RTCRegion[];
+ const members = guild.memberCount;
+ const online = guild.approximatePresenceCount ?? 0;
+ const offline = members - online;
+
guildAbout.push(
- `**Owner:** ${escapeMarkdown(guild.members.cache.get(guild.ownerId)?.user.tag ?? '¯\\_(ツ)_/¯')}`,
- `**Created** ${timestampAndDelta(guild.createdAt, 'd')}`,
- `**Members:** ${guild.memberCount.toLocaleString() ?? 0} (${emojis.onlineCircle} ${
- guild.approximatePresenceCount?.toLocaleString() ?? 0
- }, ${emojis.offlineCircle} ${(guild.memberCount - (guild.approximatePresenceCount ?? 0)).toLocaleString() ?? 0})`,
- `**Regions:** ${guildRegions.map((region) => mappings.regions[region] || region).join(', ')}`
+ embedField`
+ Owner ${escapeMarkdown(guild.members.cache.get(guild.ownerId)?.user.tag ?? '¯\\_(ツ)_/¯')}
+ Created ${timestampAndDelta(guild.createdAt, 'd')}
+ Members ${members} (${emojis.onlineCircle} ${online}, ${emojis.offlineCircle} ${offline})
+ Regions ${guildRegions.map((region) => mappings.regions[region] || region).join(', ')}
+ Boosts ${guild.premiumSubscriptionCount && `Level ${guild.premiumTier} with ${guild.premiumSubscriptionCount} boosts`}`
);
- if (guild.premiumSubscriptionCount)
- guildAbout.push(`**Boosts:** Level ${guild.premiumTier} with ${guild.premiumSubscriptionCount ?? 0} boosts`);
+
if (guild.members.me?.permissions.has(PermissionFlagsBits.ManageGuild) && guild.vanityURLCode) {
const vanityInfo: Vanity = await guild.fetchVanityData();
- guildAbout.push(`**Vanity URL:** discord.gg/${vanityInfo.code}`, `**Vanity Uses:** ${vanityInfo.uses?.toLocaleString()}`);
+ guildAbout.push(
+ embedField`
+ Vanity URL ${`discord.gg/${vanityInfo.code}`}
+ Vanity Uses ${vanityInfo.uses}`
+ );
}
- if (guild.icon) guildAbout.push(`**Icon:** [link](${guild.iconURL({ size: 4096, extension: 'png' })})`);
- if (guild.banner) guildAbout.push(`**Banner:** [link](${guild.bannerURL({ size: 4096, extension: 'png' })})`);
- if (guild.splash) guildAbout.push(`**Splash:** [link](${guild.splashURL({ size: 4096, extension: 'png' })})`);
+ guildAbout.push(
+ embedField`
+ Icon ${guild.icon && `[link](${guild.iconURL({ size: 4096, extension: 'png' })})`}
+ Banner ${guild.banner && `[link](${guild.bannerURL({ size: 4096, extension: 'png' })})`}
+ Splash ${guild.splash && `[link](${guild.splashURL({ size: 4096, extension: 'png' })})`}`
+ );
} else {
+ const members = guild.approximateMemberCount;
+ const online = guild.approximatePresenceCount;
+ const offline = members - online;
+
guildAbout.push(
- `**Members:** ${guild.approximateMemberCount?.toLocaleString() ?? 0} (${emojis.onlineCircle} ${
- guild.approximatePresenceCount?.toLocaleString() ?? 0
- }, ${emojis.offlineCircle} ${(
- (guild.approximateMemberCount ?? 0) - (guild.approximatePresenceCount ?? 0)
- ).toLocaleString()})`,
- `**Emojis:** ${(guild as GuildPreview).emojis.size?.toLocaleString() ?? 0}`,
- `**Stickers:** ${(guild as GuildPreview).stickers.size}`
+ embedField`
+ Members ${members} (${emojis.onlineCircle} ${online}, ${emojis.offlineCircle} ${offline})
+ Emojis ${guild.emojis.size}
+ Stickers ${guild.stickers.size}`
);
}
- embed.addFields({ name: '» About', value: guildAbout.join('\n') });
+ embed.addFields({
+ name: '» About',
+ // filter out anything that is undefined
+ value: guildAbout.filter((v) => v !== undefined).join('\n')
+ });
}
private generateStatsField(embed: EmbedBuilder, guild: Guild | GuildPreview) {
if (!(guild instanceof Guild)) return;
- const guildStats: string[] = [];
-
const channelTypes = (
[
['Text', [ChannelType.GuildText]],
['Voice', [ChannelType.GuildVoice]],
- ['News', [ChannelType.GuildNews]],
+ ['News', [ChannelType.GuildAnnouncement]],
['Stage', [ChannelType.GuildStageVoice]],
['Category', [ChannelType.GuildCategory]],
- ['Thread', [ChannelType.GuildNewsThread, ChannelType.GuildPrivateThread, ChannelType.GuildPublicThread]]
+ ['Thread', [ChannelType.AnnouncementThread, ChannelType.PrivateThread, ChannelType.PublicThread]]
] as const
).map(
(type) =>
@@ -205,30 +225,25 @@ export default class GuildInfoCommand extends BotCommand {
[GuildPremiumTier.None]: 0
} as const;
- guildStats.push(
- `**Channels:** ${guild.channels.cache.size.toLocaleString()} / 500 (${channelTypes.join(', ')})`,
- // subtract 1 for @everyone role
- `**Roles:** ${((guild.roles.cache.size ?? 0) - 1).toLocaleString()} / 250`,
- `**Emojis:** ${guild.emojis.cache.size?.toLocaleString() ?? 0} / ${EmojiTierMap[guild.premiumTier]}`,
- `**Stickers:** ${guild.stickers.cache.size?.toLocaleString() ?? 0} / ${StickerTierMap[guild.premiumTier]}`
- );
+ const guildStats = embedField`
+ Channels ${guild.channels.cache.size} / 500 (${channelTypes.join(', ')})
+ Roles ${guild.roles.cache.size - 1 /* account for @everyone role */} / 250
+ Emojis ${guild.emojis.cache.size} / ${EmojiTierMap[guild.premiumTier]}
+ Stickers ${guild.stickers.cache.size} / ${StickerTierMap[guild.premiumTier]}`;
- embed.addFields({ name: '» Stats', value: guildStats.join('\n') });
+ embed.addFields({ name: '» Stats', value: guildStats });
}
private generateSecurityField(embed: EmbedBuilder, guild: Guild | GuildPreview) {
if (!(guild instanceof Guild)) return;
- const guildSecurity: string[] = [];
-
- guildSecurity.push(
- `**Verification Level:** ${MappedGuildVerificationLevel[guild.verificationLevel]}`,
- `**Explicit Content Filter:** ${MappedGuildExplicitContentFilter[guild.explicitContentFilter]}`,
- `**Default Message Notifications:** ${MappedGuildDefaultMessageNotifications[guild.defaultMessageNotifications]}`,
- `**2FA Required:** ${guild.mfaLevel === GuildMFALevel.Elevated ? 'True' : 'False'}`
- );
+ const guildSecurity = embedField`
+ Verification Level ${MappedGuildVerificationLevel[guild.verificationLevel]}
+ Explicit Content Filter ${MappedGuildExplicitContentFilter[guild.explicitContentFilter]}
+ Default Message Notifications ${MappedGuildDefaultMessageNotifications[guild.defaultMessageNotifications]}
+ 2FA Required ${guild.mfaLevel === GuildMFALevel.Elevated ? 'True' : 'False'}`;
- embed.addFields({ name: '» Security', value: guildSecurity.join('\n') });
+ embed.addFields({ name: '» Security', value: guildSecurity });
}
}
diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts
index 1680b75..565fc25 100644
--- a/src/commands/info/help.ts
+++ b/src/commands/info/help.ts
@@ -20,9 +20,11 @@ import {
ButtonStyle,
EmbedBuilder
} from 'discord.js';
-import { default as Fuse } from 'fuse.js';
import packageDotJSON from '../../../package.json' assert { type: 'json' };
+// todo: remove this bullshit once typescript gets its shit together
+const Fuse = (await import('fuse.js')).default as unknown as typeof import('fuse.js').default;
+
assert(Fuse);
assert(packageDotJSON);
diff --git a/src/commands/info/inviteInfo.ts b/src/commands/info/inviteInfo.ts
index bf66a4c..123063d 100644
--- a/src/commands/info/inviteInfo.ts
+++ b/src/commands/info/inviteInfo.ts
@@ -1,4 +1,5 @@
import { Arg, ArgType, BotCommand, colors, type CommandMessage, type SlashMessage } from '#lib';
+import { embedField } from '#lib/common/tags.js';
import { ApplicationCommandOptionType, EmbedBuilder, Invite } from 'discord.js';
export default class InviteInfoCommand extends BotCommand {
@@ -38,8 +39,10 @@ export default class InviteInfoCommand extends BotCommand {
}
private generateAboutField(embed: EmbedBuilder, invite: Invite) {
- const about = [`**code:** ${invite.code}`, `**channel:** ${invite.channel!.name}`];
+ const about = embedField`
+ Code ${invite.code}
+ Channel ${invite.channel!.name}`;
- embed.addFields({ name: '» About', value: about.join('\n') });
+ embed.addFields({ name: '» About', value: about });
}
}
diff --git a/src/commands/info/ping.ts b/src/commands/info/ping.ts
index ad58cc0..66cdf01 100644
--- a/src/commands/info/ping.ts
+++ b/src/commands/info/ping.ts
@@ -43,7 +43,7 @@ export default class PingCommand extends BotCommand {
.setColor(colors.default)
.setTimestamp();
return message.util.reply({
- content: null,
+ content: '',
embeds: [embed]
});
}
diff --git a/src/commands/info/snowflake.ts b/src/commands/info/snowflake.ts
index ba93611..1d3533e 100644
--- a/src/commands/info/snowflake.ts
+++ b/src/commands/info/snowflake.ts
@@ -1,5 +1,5 @@
import { BotCommand, colors, timestamp, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
-import { stripIndent } from '#tags';
+import { embedField } from '#tags';
import {
ApplicationCommandOptionType,
ChannelType,
@@ -50,7 +50,7 @@ export default class SnowflakeCommand extends BotCommand {
snowflakeEmbed.setTitle(`:snowflake: DM with ${escapeMarkdown(channel.recipient?.tag ?? '¯\\_(ツ)_/¯')} \`[Channel]\``);
} else if (
channel.type === ChannelType.GuildCategory ||
- channel.type === ChannelType.GuildNews ||
+ channel.type === ChannelType.GuildAnnouncement ||
channel.type === ChannelType.GuildText ||
channel.type === ChannelType.GuildVoice ||
channel.type === ChannelType.GuildStageVoice ||
@@ -68,10 +68,10 @@ export default class SnowflakeCommand extends BotCommand {
// Guild
if (this.client.guilds.cache.has(snowflake)) {
const guild = this.client.guilds.cache.get(snowflake)!;
- const guildInfo = stripIndent`
- **Name:** ${escapeMarkdown(guild.name)}
- **Owner:** ${escapeMarkdown(this.client.users.cache.get(guild.ownerId)?.tag ?? '¯\\_(ツ)_/¯')} (${guild.ownerId})
- **Members:** ${guild.memberCount?.toLocaleString()}`;
+ const guildInfo = embedField`
+ Name ${escapeMarkdown(guild.name)}
+ Owner ${escapeMarkdown(this.client.users.cache.get(guild.ownerId)?.tag ?? '¯\\_(ツ)_/¯')} (${guild.ownerId})
+ Members ${guild.memberCount?.toLocaleString()}`;
if (guild.icon) snowflakeEmbed.setThumbnail(guild.iconURL({ size: 2048 })!);
snowflakeEmbed.addFields({ name: '» Server Info', value: guildInfo });
snowflakeEmbed.setTitle(`:snowflake: ${escapeMarkdown(guild.name)} \`[Server]\``);
@@ -81,8 +81,8 @@ export default class SnowflakeCommand extends BotCommand {
const fetchedUser = await this.client.users.fetch(`${snowflake}`).catch(() => undefined);
if (this.client.users.cache.has(snowflake) || fetchedUser) {
const user = (this.client.users.cache.get(snowflake) ?? fetchedUser)!;
- const userInfo = stripIndent`
- **Name:** <@${user.id}> (${escapeMarkdown(user.tag)})`;
+ const userInfo = embedField`
+ Name ${`<@${user.id}> (${escapeMarkdown(user.tag)})`}`;
if (user.avatar) snowflakeEmbed.setThumbnail(user.avatarURL({ size: 2048 })!);
snowflakeEmbed.addFields({ name: '» User Info', value: userInfo });
snowflakeEmbed.setTitle(`:snowflake: ${escapeMarkdown(user.tag)} \`[User]\``);
@@ -91,9 +91,9 @@ export default class SnowflakeCommand extends BotCommand {
// Emoji
if (this.client.emojis.cache.has(snowflake)) {
const emoji = this.client.emojis.cache.get(snowflake)!;
- const emojiInfo = stripIndent`
- **Name:** ${escapeMarkdown(emoji.name ?? '¯\\_(ツ)_/¯')}
- **Animated:** ${emoji.animated}`;
+ const emojiInfo = embedField`
+ Name ${escapeMarkdown(emoji.name ?? '¯\\_(ツ)_/¯')}
+ Animated ${emoji.animated}`;
if (emoji.url) snowflakeEmbed.setThumbnail(emoji.url);
snowflakeEmbed.addFields({ name: '» Emoji Info', value: emojiInfo });
snowflakeEmbed.setTitle(`:snowflake: ${escapeMarkdown(emoji.name ?? '¯\\_(ツ)_/¯')} \`[Emoji]\``);
@@ -102,13 +102,13 @@ export default class SnowflakeCommand extends BotCommand {
// Role
if (message.guild && message.guild.roles.cache.has(snowflake)) {
const role = message.guild.roles.cache.get(snowflake)!;
- const roleInfo = stripIndent`
- **Name:** <@&${role.id}> (${escapeMarkdown(role.name)})
- **Members:** ${role.members.size}
- **Hoisted:** ${role.hoist}
- **Managed:** ${role.managed}
- **Position:** ${role.position}
- **Hex Color:** ${role.hexColor}`;
+ const roleInfo = embedField`
+ Name ${`<@&${role.id}> (${escapeMarkdown(role.name)})`}
+ Members ${role.members.size}
+ Hoisted ${role.hoist}
+ Managed ${role.managed}
+ Position ${role.position}
+ Hex Color ${role.hexColor}`;
if (role.color) snowflakeEmbed.setColor(role.color);
snowflakeEmbed.addFields({ name: '» Role Info', value: roleInfo });
snowflakeEmbed.setTitle(`:snowflake: ${escapeMarkdown(role.name)} \`[Role]\``);
@@ -116,12 +116,12 @@ export default class SnowflakeCommand extends BotCommand {
// SnowflakeInfo
const deconstructedSnowflake: DeconstructedSnowflake = SnowflakeUtil.deconstruct(snowflake);
- const snowflakeInfo = stripIndent`
- **Timestamp:** ${deconstructedSnowflake.timestamp}
- **Created:** ${timestamp(new Date(Number(deconstructedSnowflake.timestamp)))}
- **Worker ID:** ${deconstructedSnowflake.workerId}
- **Process ID:** ${deconstructedSnowflake.processId}
- **Increment:** ${deconstructedSnowflake.increment}`;
+ const snowflakeInfo = embedField`
+ Timestamp ${deconstructedSnowflake.timestamp}
+ Created ${timestamp(new Date(Number(deconstructedSnowflake.timestamp)))}
+ Worker ID ${deconstructedSnowflake.workerId}
+ Process ID ${deconstructedSnowflake.processId}
+ Increment ${deconstructedSnowflake.increment}`;
snowflakeEmbed.addFields({ name: '» Snowflake Info', value: snowflakeInfo });
return await message.util.reply({ embeds: [snowflakeEmbed] });
diff --git a/src/commands/info/userInfo.ts b/src/commands/info/userInfo.ts
index 25621fa..22088fe 100644
--- a/src/commands/info/userInfo.ts
+++ b/src/commands/info/userInfo.ts
@@ -13,6 +13,7 @@ import {
type OptArgType,
type SlashMessage
} from '#lib';
+import { embedField } from '#lib/common/tags.js';
import {
ActivityType,
ApplicationCommandOptionType,
@@ -20,7 +21,6 @@ import {
EmbedBuilder,
escapeMarkdown,
PermissionFlagsBits,
- TeamMemberMembershipState,
UserFlags,
type APIApplication,
type ApplicationFlagsString,
@@ -128,62 +128,69 @@ export default class UserInfoCommand extends BotCommand {
await this.generateBotField(userEmbed, user);
- if (emojis)
+ if (emojis) {
userEmbed.setDescription(
`\u200B${emojis.filter((e) => e).join(' ')}${
userEmbed.data.description?.length ? `\n\n${userEmbed.data.description}` : ''
}`
); // zero width space
+ }
+
return userEmbed;
}
public static async generateGeneralInfoField(embed: EmbedBuilder, user: User, title = '» General Information') {
- // General Info
- const generalInfo = [
- `**Mention:** <@${user.id}>`,
- `**ID:** ${user.id}`,
- `**Created:** ${timestampAndDelta(user.createdAt, 'd')}`
- ];
- if (user.accentColor !== null) generalInfo.push(`**Accent Color:** ${user.hexAccentColor}`);
- if (user.banner) generalInfo.push(`**Banner:** [link](${user.bannerURL({ extension: 'png', size: 4096 })})`);
-
- const pronouns = await Promise.race([user.client.utils.getPronounsOf(user), sleep(2 * Time.Second)]); // cut off request after 2 seconds
-
- if (pronouns && typeof pronouns === 'string' && pronouns !== 'Unspecified') generalInfo.push(`**Pronouns:** ${pronouns}`);
-
- embed.addFields({ name: title, value: generalInfo.join('\n') });
+ const pronouns = await Promise.race([
+ user.client.utils.getPronounsOf(user),
+ // cut off request after 2 seconds
+ sleep(2 * Time.Second)
+ ]);
+
+ const generalInfo = embedField`
+ Mention ${`<@${user.id}>`}
+ ID ${user.id}
+ Created ${timestampAndDelta(user.createdAt, 'd')}
+ Accent Color ${user.hexAccentColor}
+ Banner ${user.banner && `[link](${user.bannerURL({ extension: 'png', size: 4096 })})`}
+ Pronouns ${typeof pronouns === 'string' && pronouns !== 'Unspecified' && pronouns}`;
+
+ embed.addFields({ name: title, value: generalInfo });
}
public static generateServerInfoField(embed: EmbedBuilder, member?: GuildMember | undefined, title = '» Server Information') {
if (!member) return;
- // Server User Info
- const serverUserInfo = [];
- if (member.joinedTimestamp)
- serverUserInfo.push(
- `**${member.guild!.ownerId == member.user.id ? 'Created Server' : 'Joined'}:** ${timestampAndDelta(
- member.joinedAt!,
- 'd'
- )}`
- );
- if (member.premiumSince) serverUserInfo.push(`**Booster Since:** ${timestampAndDelta(member.premiumSince, 'd')}`);
- if (member.displayHexColor) serverUserInfo.push(`**Display Color:** ${member.displayHexColor}`);
- if (member.user.id == mappings.users['IRONM00N'] && member.guild?.id == mappings.guilds["Moulberry's Bush"])
- serverUserInfo.push(`**General Deletions:** 1⅓`);
- if (
- ([mappings.users['nopo'], mappings.users['Bestower']] as const).includes(member.user.id) &&
- member.guild.id == mappings.guilds["Moulberry's Bush"]
- )
- serverUserInfo.push(`**General Deletions:** ⅓`);
- if (member?.nickname) serverUserInfo.push(`**Nickname:** ${escapeMarkdown(member?.nickname)}`);
- if (serverUserInfo.length) embed.addFields({ name: title, value: serverUserInfo.join('\n') });
+ const isGuildOwner = member.guild.ownerId === member.id;
+
+ const deletions = (() => {
+ if (member.guild.id !== mappings.guilds["Moulberry's Bush"]) return null;
+
+ switch (member.id) {
+ case mappings.users['IRONM00N']:
+ return '1⅓';
+ case mappings.users['nopo']:
+ case mappings.users['Bestower']:
+ return '⅓';
+ default:
+ return null;
+ }
+ })();
+
+ const serverUserInfo = embedField`
+ Created Server ${member.joinedAt && isGuildOwner && timestampAndDelta(member.joinedAt!, 'd')}
+ Joined ${member.joinedAt && !isGuildOwner && timestampAndDelta(member.joinedAt!, 'd')}
+ Booster Since ${member.premiumSince && timestampAndDelta(member.premiumSince, 'd')}
+ Display Color ${member.displayHexColor}
+ #general Deletions ${deletions}
+ Nickname ${member.nickname && escapeMarkdown(member.nickname)}`;
+
+ if (serverUserInfo.length) embed.addFields({ name: title, value: serverUserInfo });
}
public static generatePresenceField(embed: EmbedBuilder, member?: GuildMember | undefined, title = '» Presence') {
if (!member || !member.presence) return;
if (!member.presence.status && !member.presence.clientStatus && !member.presence.activities) return;
- // User Presence Info
let customStatus = '';
const activitiesNames: string[] = [];
if (member.presence.activities) {
@@ -231,7 +238,7 @@ export default class UserInfoCommand extends BotCommand {
const joined = roles.join(', ');
embed.addFields({
name: `» Role${roles.length - 1 ? 's' : ''} [${roles.length}]`,
- value: joined.length > 1024 ? 'Too Many Roles to Display' + '...' : joined
+ value: joined.length > 1024 ? 'Too Many Roles to Display...' : joined
});
}
@@ -242,7 +249,6 @@ export default class UserInfoCommand extends BotCommand {
) {
if (!member) return;
- // Important Perms
const perms = this.getImportantPermissions(member);
if (perms.length) embed.addFields({ name: title, value: perms.join(' ') });
@@ -282,27 +288,13 @@ export default class UserInfoCommand extends BotCommand {
return emojis.cross;
};
- const botInfo = [
- `**Publicity:** ${applicationInfo.bot_public ? 'Public' : 'Private'}`,
- `**Requires Code Grant:** ${applicationInfo.bot_require_code_grant ? emojis.check : emojis.cross}`,
- `**Server Members Intent:** ${intent('GatewayGuildMembers', 'GatewayGuildMembersLimited')}`,
- `**Presence Intent:** ${intent('GatewayPresence', 'GatewayPresenceLimited')}`,
- `**Message Content Intent:** ${intent('GatewayMessageContent', 'GatewayMessageContentLimited')}`
- ];
-
- if (applicationInfo.owner || applicationInfo.team) {
- const teamMembers = applicationInfo.owner
- ? [applicationInfo.owner]
- : applicationInfo
- .team!.members.filter((tm) => tm.membership_state === TeamMemberMembershipState.Accepted)
- .map((tm) => tm.user);
- botInfo.push(
- `**Developer${teamMembers.length > 1 ? 's' : ''}:** ${teamMembers
- .map((m) => `${m.username}#${m.discriminator}`)
- .join(', ')}`
- );
- }
+ const botInfo = embedField`
+ Publicity ${applicationInfo.bot_public ? 'Public' : 'Private'}
+ Code Grant ${applicationInfo.bot_require_code_grant ? 'Required' : 'Not Required'}
+ Server Members Intent ${intent('GatewayGuildMembers', 'GatewayGuildMembersLimited')}
+ Presence Intent ${intent('GatewayPresence', 'GatewayPresenceLimited')}
+ Message Content Intent ${intent('GatewayMessageContent', 'GatewayMessageContentLimited')}`;
- if (botInfo.length) embed.addFields({ name: title, value: botInfo.join('\n') });
+ embed.addFields({ name: title, value: botInfo });
}
}
diff --git a/src/commands/leveling/level.ts b/src/commands/leveling/level.ts
index 869140d..bf4ca9b 100644
--- a/src/commands/leveling/level.ts
+++ b/src/commands/leveling/level.ts
@@ -9,11 +9,11 @@ import {
type SlashMessage
} from '#lib';
import canvas from '@napi-rs/canvas';
-import { SimplifyNumber } from '@notenoughupdates/simplify-number';
+import { simplifyNumber } from '@tanzanite/simplify-number';
import assert from 'assert/strict';
import { ApplicationCommandOptionType, AttachmentBuilder, Guild, PermissionFlagsBits, User } from 'discord.js';
+
assert(canvas);
-assert(SimplifyNumber);
export default class LevelCommand extends BotCommand {
public constructor() {
@@ -119,9 +119,9 @@ export default class LevelCommand extends BotCommand {
// Draw level data text
ctx.fillStyle = white;
- const xpTxt = `${SimplifyNumber(currentLevelXpProgress)}/${SimplifyNumber(xpForNextLevel)}`;
+ const xpTxt = `${simplifyNumber(currentLevelXpProgress)}/${simplifyNumber(xpForNextLevel)}`;
- const rankTxt = SimplifyNumber(rank.indexOf(rank.find((x) => x.user === user.id)!) + 1);
+ const rankTxt = simplifyNumber(rank.indexOf(rank.find((x) => x.user === user.id)!) + 1);
ctx.fillText(`Level: ${userLevel} XP: ${xpTxt} Rank: ${rankTxt}`, AVATAR_SIZE + 70, AVATAR_SIZE - 20);
// Return image in buffer form
diff --git a/src/commands/leveling/setLevel.ts b/src/commands/leveling/setLevel.ts
index 6f6f69e..3a995a2 100644
--- a/src/commands/leveling/setLevel.ts
+++ b/src/commands/leveling/setLevel.ts
@@ -1,4 +1,5 @@
-import { AllowedMentions, BotCommand, emojis, format, Level, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
+import { AllowedMentions, BotCommand, emojis, Level, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
+import { commas } from '#lib/common/tags.js';
import assert from 'assert/strict';
import { ApplicationCommandOptionType } from 'discord.js';
@@ -42,20 +43,27 @@ export default class SetLevelCommand extends BotCommand {
assert(message.inGuild());
assert(user.id);
- if (isNaN(level) || !Number.isInteger(level))
+ if (isNaN(level) || !Number.isInteger(level)) {
return await message.util.reply(`${emojis.error} Provide a valid number to set the user's level to.`);
- if (level > 6553 || level < 0)
- return await message.util.reply(`${emojis.error} You cannot set a level higher than **6,553**.`);
+ }
+
+ if (level > Level.MAX_LEVEL || level < 0) {
+ return await message.util.reply(commas`${emojis.error} You cannot set a level higher than **${Level.MAX_LEVEL}**.`);
+ }
const [levelEntry] = await Level.findOrBuild({
- where: { user: user.id, guild: message.guild.id },
- defaults: { user: user.id, guild: message.guild.id, xp: 0 }
+ where: {
+ user: user.id,
+ guild: message.guild.id
+ }
});
- await levelEntry.update({ xp: Level.convertLevelToXp(level), user: user.id, guild: message.guild.id });
+
+ const xp = Level.convertLevelToXp(level);
+
+ await levelEntry.update({ xp, user: user.id, guild: message.guild.id });
+
return await message.util.send({
- content: `Successfully set level of <@${user.id}> to ${format.input(level.toLocaleString())} (${format.input(
- levelEntry.xp.toLocaleString()
- )} XP)`,
+ content: commas`Successfully set level of <@${user.id}> to **${level}** (**${xp}** xp)`,
allowedMentions: AllowedMentions.none()
});
}
diff --git a/src/commands/leveling/setXp.ts b/src/commands/leveling/setXp.ts
index 8c3b86f..270ad68 100644
--- a/src/commands/leveling/setXp.ts
+++ b/src/commands/leveling/setXp.ts
@@ -1,4 +1,6 @@
-import { AllowedMentions, BotCommand, emojis, format, Level, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
+import { AllowedMentions, BotCommand, emojis, Level, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
+import { commas } from '#lib/common/tags.js';
+import { input } from '#lib/utils/Format.js';
import assert from 'assert/strict';
import { ApplicationCommandOptionType } from 'discord.js';
@@ -44,22 +46,36 @@ export default class SetXpCommand extends BotCommand {
assert(user.id);
if (isNaN(xp)) return await message.util.reply(`${emojis.error} Provide a valid number.`);
- if (xp > 2147483647 || xp < 0)
+
+ if (xp > Level.MAX_XP || xp < 0) {
return await message.util.reply(
- `${emojis.error} Provide an positive integer under **2,147,483,647** to set the user's xp to.`
+ commas`${emojis.error} Provide an positive integer under **${Level.MAX_XP}** to set the user's xp to.`
);
+ }
const [levelEntry] = await Level.findOrBuild({
- where: { user: user.id, guild: message.guild.id },
- defaults: { user: user.id, guild: message.guild.id }
+ where: {
+ user: user.id,
+ guild: message.guild.id
+ }
});
- await levelEntry.update({ xp: xp, user: user.id, guild: message.guild.id });
+ const res = await levelEntry
+ .update({ xp: xp, user: user.id, guild: message.guild.id })
+ .catch((e) => (e instanceof Error ? e : null));
+
+ xp = levelEntry.xp;
+ const level = Level.convertXpToLevel(xp);
+
+ if (res instanceof Error || res == null) {
+ return await message.util.reply({
+ content: commas`Unable to set <@${user.id}>'s xp to **${xp}** with error ${input(res?.message ?? '¯\\_(ツ)_/¯')}.`,
+ allowedMentions: AllowedMentions.none()
+ });
+ }
return await message.util.send({
- content: `Successfully set <@${user.id}>'s xp to ${format.input(levelEntry.xp.toLocaleString())} (level ${format.input(
- Level.convertXpToLevel(levelEntry.xp).toLocaleString()
- )}).`,
+ content: commas`${emojis.success} Successfully set <@${user.id}>'s xp to **${xp}** (level **${level}**).`,
allowedMentions: AllowedMentions.none()
});
}
diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts
index aee8805..ae77cde 100644
--- a/src/commands/moderation/ban.ts
+++ b/src/commands/moderation/ban.ts
@@ -96,7 +96,9 @@ export default class BanCommand extends BotCommand {
if (!user) return message.util.reply(`${emojis.error} Invalid user.`);
const useForce = args.force && message.author.isOwner();
- const canModerateResponse = member ? await Moderation.permissionCheck(message.member, member, 'ban', true, useForce) : true;
+ const canModerateResponse = member
+ ? await Moderation.permissionCheck(message.member, member, Moderation.Action.Ban, true, useForce)
+ : true;
if (canModerateResponse !== true) {
return await message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/block.ts b/src/commands/moderation/block.ts
index a5ad31d..da1dec8 100644
--- a/src/commands/moderation/block.ts
+++ b/src/commands/moderation/block.ts
@@ -83,7 +83,7 @@ export default class BlockCommand extends BotCommand {
return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`);
const useForce = args.force && message.author.isOwner();
- const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'block', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, Moderation.Action.Block, true, useForce);
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/evidence.ts b/src/commands/moderation/evidence.ts
index 9a5e70f..b7c020a 100644
--- a/src/commands/moderation/evidence.ts
+++ b/src/commands/moderation/evidence.ts
@@ -10,8 +10,8 @@ import {
type CommandMessage,
type SlashMessage
} from '#lib';
+import { Argument, ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo';
import assert from 'assert/strict';
-import { Argument, ArgumentGeneratorReturn } from 'discord-akairo';
import { ApplicationCommandOptionType, type Message } from 'discord.js';
export default class EvidenceCommand extends BotCommand {
diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts
index 82ddce4..d757e91 100644
--- a/src/commands/moderation/kick.ts
+++ b/src/commands/moderation/kick.ts
@@ -70,7 +70,7 @@ export default class KickCommand extends BotCommand {
if (!member)
return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`);
const useForce = force && message.author.isOwner();
- const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'kick', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, Moderation.Action.Kick, true, useForce);
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts
index dcab9ef..649e44f 100644
--- a/src/commands/moderation/modlog.ts
+++ b/src/commands/moderation/modlog.ts
@@ -12,12 +12,11 @@ import {
type CommandMessage,
type SlashMessage
} from '#lib';
+import { embedField } from '#lib/common/tags.js';
import assert from 'assert/strict';
import { ApplicationCommandOptionType, escapeMarkdown, User } from 'discord.js';
export default class ModlogCommand extends BotCommand {
- public static separator = '\n━━━━━━━━━━━━━━━\n';
-
public constructor() {
super('modlog', {
aliases: ['modlog', 'modlogs'],
@@ -62,44 +61,63 @@ export default class ModlogCommand extends BotCommand {
const logs = await ModLog.findAll({
where: {
guild: message.guild.id,
- user: foundUser.id
+ user: foundUser.id,
+ pseudo: false
},
order: [['createdAt', 'ASC']]
});
- const niceLogs = logs
- .filter((log) => !log.pseudo && !(!hidden && log.hidden))
- .map((log) => ModlogCommand.generateModlogInfo(log, false, false));
- if (niceLogs.length < 1) return message.util.reply(`${emojis.error} **${foundUser.tag}** does not have any modlogs.`);
+ const niceLogs = logs.filter((log) => !log.hidden || hidden).map((log) => generateModlogInfo(log, false, false));
+
+ if (niceLogs.length < 1) {
+ return message.util.reply(`${emojis.error} **${foundUser.tag}** does not have any modlogs.`);
+ }
+
const chunked: string[][] = chunk(niceLogs, 4);
const embedPages = chunked.map((chunk) => ({
title: `${foundUser.tag}'s Modlogs`,
- description: chunk.join(ModlogCommand.separator),
+ description: chunk.join(modlogSeparator),
color: colors.default
}));
return await ButtonPaginator.send(message, embedPages, undefined, true);
} else if (search) {
const entry = await ModLog.findByPk(search as string);
- if (!entry || entry.pseudo || (entry.hidden && !hidden))
+
+ if (!entry || entry.pseudo || (entry.hidden && !hidden)) {
return message.util.send(`${emojis.error} That modlog does not exist.`);
- if (entry.guild !== message.guild.id) return message.util.reply(`${emojis.error} This modlog is from another server.`);
+ }
+
+ if (entry.guild !== message.guild.id) {
+ return message.util.reply(`${emojis.error} This modlog is from another server.`);
+ }
+
const embed = {
title: `Case ${entry.id}`,
- description: ModlogCommand.generateModlogInfo(entry, true, false),
+ description: generateModlogInfo(entry, true, false),
color: colors.default
};
return await ButtonPaginator.send(message, [embed]);
}
}
+}
- public static generateModlogInfo(log: ModLog, showUser: boolean, userFacing: boolean): string {
- const trim = (str: string): string => (str.endsWith('\n') ? str.substring(0, str.length - 1).trim() : str.trim());
- const modLog = [`**Case ID:** ${escapeMarkdown(log.id)}`, `**Type:** ${log.type.toLowerCase()}`];
- if (showUser) modLog.push(`**User:** <@!${log.user}>`);
- if (!userFacing) modLog.push(`**Moderator:** <@!${log.moderator}>`);
- if (log.duration) modLog.push(`**Duration:** ${humanizeDuration(log.duration)}`);
- modLog.push(`**Reason:** ${trim(log.reason ?? 'No Reason Specified.')}`);
- modLog.push(`**Date:** ${timestamp(log.createdAt)}`);
- if (log.evidence && !userFacing) modLog.push(`**Evidence:** ${trim(log.evidence)}`);
- return modLog.join(`\n`);
+export const modlogSeparator = '\n━━━━━━━━━━━━━━━\n';
+
+const trim = (str: string): string => {
+ if (str.endsWith('\n')) {
+ return str.substring(0, str.length - 1).trim();
+ } else {
+ return str.trim();
}
+};
+
+export function generateModlogInfo(log: ModLog, showUser: boolean, userFacing: boolean): string {
+ return embedField`
+ Case ID ${escapeMarkdown(log.id)}
+ Type ${log.type.toLowerCase()}
+ User ${showUser && `<@!${log.user}>`}
+ Moderator ${!userFacing && `<@!${log.moderator}>`}
+ Duration ${log.duration && humanizeDuration(log.duration)}
+ Reason ${trim(log.reason ?? 'No Reason Specified.')}
+ Date ${timestamp(log.createdAt)}
+ Evidence ${log.evidence && !userFacing && trim(log.evidence)}`;
}
diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts
index 9ffaf8d..a64dc99 100644
--- a/src/commands/moderation/mute.ts
+++ b/src/commands/moderation/mute.ts
@@ -78,7 +78,7 @@ export default class MuteCommand extends BotCommand {
return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`);
const useForce = args.force && message.author.isOwner();
- const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'mute', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, Moderation.Action.Mute, true, useForce);
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/myLogs.ts b/src/commands/moderation/myLogs.ts
index 8faca8c..e3f5f10 100644
--- a/src/commands/moderation/myLogs.ts
+++ b/src/commands/moderation/myLogs.ts
@@ -12,7 +12,7 @@ import {
import { ApplicationCommandOptionType } from 'discord.js';
import { input, sanitizeInputForDiscord } from '../../../lib/utils/Format.js';
-import ModlogCommand from './modlog.js';
+import { generateModlogInfo, modlogSeparator } from './modlog.js';
export default class MyLogsCommand extends BotCommand {
public constructor() {
super('myLogs', {
@@ -50,14 +50,14 @@ export default class MyLogsCommand extends BotCommand {
const logs = await ModLog.findAll({
where: {
guild: guild.id,
- user: message.author.id
+ user: message.author.id,
+ pseudo: false,
+ hidden: false
},
order: [['createdAt', 'ASC']]
});
- const niceLogs = logs
- .filter((log) => !log.pseudo && !log.hidden)
- .map((log) => ModlogCommand.generateModlogInfo(log, false, true));
+ const niceLogs = logs.map((log) => generateModlogInfo(log, false, true));
if (niceLogs.length < 1) return message.util.reply(`${emojis.error} You don't have any modlogs in ${input(guild.name)}.`);
@@ -65,7 +65,7 @@ export default class MyLogsCommand extends BotCommand {
const embedPages = chunked.map((chunk) => ({
title: `Your Modlogs in ${sanitizeInputForDiscord(guild.name)}`,
- description: chunk.join(ModlogCommand.separator),
+ description: chunk.join(modlogSeparator),
color: colors.default
}));
diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts
index 565f214..a664aa4 100644
--- a/src/commands/moderation/role.ts
+++ b/src/commands/moderation/role.ts
@@ -12,8 +12,8 @@ import {
type OptArgType,
type SlashMessage
} from '#lib';
+import { type ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo';
import assert from 'assert/strict';
-import { type ArgumentGeneratorReturn } from 'discord-akairo';
import { ApplicationCommandOptionType, PermissionFlagsBits, type Snowflake } from 'discord.js';
export default class RoleCommand extends BotCommand {
diff --git a/src/commands/moderation/slowmode.ts b/src/commands/moderation/slowmode.ts
index 82d0264..1256d1f 100644
--- a/src/commands/moderation/slowmode.ts
+++ b/src/commands/moderation/slowmode.ts
@@ -1,6 +1,6 @@
import { Arg, BotCommand, emojis, format, humanizeDuration, type CommandMessage, type OptArgType, type SlashMessage } from '#lib';
+import { Argument } from '@notenoughupdates/discord-akairo';
import assert from 'assert/strict';
-import { Argument } from 'discord-akairo';
import { ApplicationCommandOptionType, ChannelType } from 'discord.js';
export default class SlowmodeCommand extends BotCommand {
@@ -30,7 +30,7 @@ export default class SlowmodeCommand extends BotCommand {
retry: '{error} Choose a valid channel.',
optional: true,
slashType: ApplicationCommandOptionType.Channel,
- channelTypes: [ChannelType.GuildText, ChannelType.GuildPrivateThread, ChannelType.GuildPublicThread]
+ channelTypes: [ChannelType.GuildText, ChannelType.PrivateThread, ChannelType.PublicThread]
}
],
slash: true,
@@ -55,7 +55,7 @@ export default class SlowmodeCommand extends BotCommand {
if (
args.channel.type !== ChannelType.GuildText &&
- args.channel.type !== ChannelType.GuildNews &&
+ args.channel.type !== ChannelType.GuildAnnouncement &&
args.channel.type !== ChannelType.GuildVoice &&
!args.channel.isThread()
)
diff --git a/src/commands/moderation/timeout.ts b/src/commands/moderation/timeout.ts
index 7bb02f7..db6ab56 100644
--- a/src/commands/moderation/timeout.ts
+++ b/src/commands/moderation/timeout.ts
@@ -73,7 +73,13 @@ export default class TimeoutCommand extends BotCommand {
return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`);
const useForce = args.force && message.author.isOwner();
- const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'timeout', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(
+ message.member,
+ member,
+ Moderation.Action.Timeout,
+ true,
+ useForce
+ );
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/unblock.ts b/src/commands/moderation/unblock.ts
index 4838392..4fdfc28 100644
--- a/src/commands/moderation/unblock.ts
+++ b/src/commands/moderation/unblock.ts
@@ -75,7 +75,13 @@ export default class UnblockCommand extends BotCommand {
return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`);
const useForce = args.force && message.author.isOwner();
- const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'unblock', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(
+ message.member,
+ member,
+ Moderation.Action.Unblock,
+ true,
+ useForce
+ );
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts
index a4d348d..c8fccc8 100644
--- a/src/commands/moderation/unmute.ts
+++ b/src/commands/moderation/unmute.ts
@@ -66,7 +66,13 @@ export default class UnmuteCommand extends BotCommand {
const member = message.guild.members.cache.get(user.id)!;
const useForce = force && message.author.isOwner();
- const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'unmute', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(
+ message.member,
+ member,
+ Moderation.Action.Unmute,
+ true,
+ useForce
+ );
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/untimeout.ts b/src/commands/moderation/untimeout.ts
index 3775c65..64364e5 100644
--- a/src/commands/moderation/untimeout.ts
+++ b/src/commands/moderation/untimeout.ts
@@ -73,7 +73,13 @@ export default class UntimeoutCommand extends BotCommand {
if (!member.isCommunicationDisabled()) return message.util.reply(`${emojis.error} That user is not timed out.`);
const useForce = args.force && message.author.isOwner();
- const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'timeout', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(
+ message.member,
+ member,
+ Moderation.Action.Untimeout,
+ true,
+ useForce
+ );
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts
index 4bc7f13..a7ed814 100644
--- a/src/commands/moderation/warn.ts
+++ b/src/commands/moderation/warn.ts
@@ -69,7 +69,7 @@ export default class WarnCommand extends BotCommand {
const member = message.guild.members.cache.get(user.id);
if (!member) return message.util.reply(`${emojis.error} I cannot warn users that are not in the server.`);
const useForce = force && message.author.isOwner();
- const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'warn', true, useForce);
+ const canModerateResponse = await Moderation.permissionCheck(message.member, member, Moderation.Action.Warn, true, useForce);
if (canModerateResponse !== true) {
return message.util.reply(canModerateResponse);
diff --git a/src/commands/moulberry-bush/capes.ts b/src/commands/moulberry-bush/capes.ts
index b292f24..a67cf46 100644
--- a/src/commands/moulberry-bush/capes.ts
+++ b/src/commands/moulberry-bush/capes.ts
@@ -13,7 +13,9 @@ import {
} from '#lib';
import assert from 'assert/strict';
import { ApplicationCommandOptionType, type APIEmbed, type AutocompleteInteraction } from 'discord.js';
-import { default as Fuse } from 'fuse.js';
+
+// todo: remove this bullshit once typescript gets its shit together
+const Fuse = (await import('fuse.js')).default as unknown as typeof import('fuse.js').default;
assert(Fuse);
@@ -85,7 +87,7 @@ export default class CapesCommand extends BotCommand {
}
} else {
const embeds: APIEmbed[] = capes.map(this.makeEmbed);
- await ButtonPaginator.send(message, embeds, null);
+ await ButtonPaginator.send(message, embeds, '');
}
}
diff --git a/src/commands/tickets/ticket-!.ts b/src/commands/tickets/ticket-!.ts
index c5c59f2..b98ec1f 100644
--- a/src/commands/tickets/ticket-!.ts
+++ b/src/commands/tickets/ticket-!.ts
@@ -1,5 +1,5 @@
import { BotCommand, deepWriteable, type SlashMessage } from '#lib';
-import { Flag, type ArgumentGeneratorReturn, type SlashOption } from 'discord-akairo';
+import { Flag, type ArgumentGeneratorReturn, type SlashOption } from '@notenoughupdates/discord-akairo';
import { ApplicationCommandOptionType } from 'discord.js';
export const ticketSubcommands = deepWriteable({
diff --git a/src/commands/utilities/activity.ts b/src/commands/utilities/activity.ts
index 89ca53e..3f17d0a 100644
--- a/src/commands/utilities/activity.ts
+++ b/src/commands/utilities/activity.ts
@@ -7,7 +7,7 @@ import {
type CommandMessage,
type SlashMessage
} from '#lib';
-import { type ArgumentGeneratorReturn, type ArgumentTypeCaster } from 'discord-akairo';
+import { type ArgumentGeneratorReturn, type ArgumentTypeCaster } from '@notenoughupdates/discord-akairo';
import { ApplicationCommandOptionType, ChannelType, type DiscordAPIError, type Snowflake } from 'discord.js';
const activityMap = {
diff --git a/src/commands/utilities/highlight-!.ts b/src/commands/utilities/highlight-!.ts
index 7716887..0e2db33 100644
--- a/src/commands/utilities/highlight-!.ts
+++ b/src/commands/utilities/highlight-!.ts
@@ -1,5 +1,5 @@
import { BotCommand, deepWriteable, Highlight, HighlightWord, type SlashMessage } from '#lib';
-import { Flag, type ArgumentGeneratorReturn, type SlashOption } from 'discord-akairo';
+import { Flag, type ArgumentGeneratorReturn, type SlashOption } from '@notenoughupdates/discord-akairo';
import { ApplicationCommandOptionType, Constants, type AutocompleteInteraction, type CacheType } from 'discord.js';
export const highlightSubcommands = deepWriteable({
diff --git a/src/commands/utilities/highlight-block.ts b/src/commands/utilities/highlight-block.ts
index b16852e..d1dec1e 100644
--- a/src/commands/utilities/highlight-block.ts
+++ b/src/commands/utilities/highlight-block.ts
@@ -1,6 +1,6 @@
import { AllowedMentions, BotCommand, emojis, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
+import { Argument, ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo';
import assert from 'assert/strict';
-import { Argument, ArgumentGeneratorReturn } from 'discord-akairo';
import { BaseChannel, GuildMember, User } from 'discord.js';
import { HighlightBlockResult } from '../../../lib/common/HighlightManager.js';
import { highlightSubcommands } from './highlight-!.js';
diff --git a/src/commands/utilities/highlight-matches.ts b/src/commands/utilities/highlight-matches.ts
index d54fd4a..0665b37 100644
--- a/src/commands/utilities/highlight-matches.ts
+++ b/src/commands/utilities/highlight-matches.ts
@@ -1,6 +1,6 @@
import { BotCommand, ButtonPaginator, chunk, colors, emojis, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
+import { type ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo';
import assert from 'assert/strict';
-import { type ArgumentGeneratorReturn } from 'discord-akairo';
import { type APIEmbed } from 'discord.js';
import { highlightSubcommands } from './highlight-!.js';
diff --git a/src/commands/utilities/highlight-unblock.ts b/src/commands/utilities/highlight-unblock.ts
index 0f2dd78..f9cc806 100644
--- a/src/commands/utilities/highlight-unblock.ts
+++ b/src/commands/utilities/highlight-unblock.ts
@@ -1,6 +1,6 @@
import { AllowedMentions, BotCommand, emojis, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
+import { Argument, ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo';
import assert from 'assert';
-import { Argument, ArgumentGeneratorReturn } from 'discord-akairo';
import { BaseChannel, GuildMember, User } from 'discord.js';
import { HighlightUnblockResult } from '../../../lib/common/HighlightManager.js';
import { highlightSubcommands } from './highlight-!.js';
diff --git a/src/commands/utilities/price.ts b/src/commands/utilities/price.ts
index 6f08aaa..0d5d4b3 100644
--- a/src/commands/utilities/price.ts
+++ b/src/commands/utilities/price.ts
@@ -1,7 +1,9 @@
import { ArgType, BotCommand, colors, emojis, format, formatList, type CommandMessage } from '#lib';
import assert from 'assert/strict';
import { ApplicationCommandOptionType, AutocompleteInteraction, EmbedBuilder } from 'discord.js';
-import { default as Fuse } from 'fuse.js';
+
+// todo: remove this bullshit once typescript gets its shit together
+const Fuse = (await import('fuse.js')).default as unknown as typeof import('fuse.js').default;
assert(Fuse);
diff --git a/src/commands/utilities/steal.ts b/src/commands/utilities/steal.ts
index a208920..bd2976a 100644
--- a/src/commands/utilities/steal.ts
+++ b/src/commands/utilities/steal.ts
@@ -1,13 +1,11 @@
import { Arg, BotCommand, emojis, format, OptArgType, regex, type CommandMessage, type SlashMessage } from '#lib';
+import { type ArgumentGeneratorReturn, type ArgumentType, type ArgumentTypeCaster } from '@notenoughupdates/discord-akairo';
import assert from 'assert/strict';
-import { type ArgumentGeneratorReturn, type ArgumentType, type ArgumentTypeCaster } from 'discord-akairo';
import { ApplicationCommandOptionType, Attachment } from 'discord.js';
-import _ from 'lodash';
+import { snakeCase } from 'lodash-es';
import { Stream } from 'stream';
import { URL } from 'url';
-assert(_);
-
// so I don't have to retype things
const enum lang {
emojiStart = 'What emoji would you like to steal?',
@@ -53,7 +51,7 @@ export default class StealCommand extends BotCommand {
const name = yield {
prompt: { start: lang.nameStart, retry: lang.nameRetry, optional: true },
- default: hasImage && message.attachments.first()!.name ? _.snakeCase(message.attachments.first()!.name!) : 'unnamed_emoji'
+ default: hasImage && message.attachments.first()!.name ? snakeCase(message.attachments.first()!.name!) : 'unnamed_emoji'
};
return { emoji, name };
diff --git a/src/commands/utilities/whoHasRole.ts b/src/commands/utilities/whoHasRole.ts
index c01a0c3..31e413d 100644
--- a/src/commands/utilities/whoHasRole.ts
+++ b/src/commands/utilities/whoHasRole.ts
@@ -77,7 +77,7 @@ export default class WhoHasRoleCommand extends BotCommand {
return await message.util.reply(`${emojis.error} No members found matching the given roles.`);
}
- return await ButtonPaginator.send(message, embedPages, null, true);
+ return await ButtonPaginator.send(message, embedPages, '', true);
}
}
diff --git a/src/commands/utilities/wolframAlpha.ts b/src/commands/utilities/wolframAlpha.ts
index 503af87..48739cf 100644
--- a/src/commands/utilities/wolframAlpha.ts
+++ b/src/commands/utilities/wolframAlpha.ts
@@ -1,7 +1,7 @@
import { AllowedMentions, BotCommand, colors, emojis, type ArgType, type CommandMessage, type SlashMessage } from '#lib';
-import { initializeClass as WolframAlphaAPI } from '@notenoughupdates/wolfram-alpha-api';
+import { initializeClass as WolframAlphaAPI } from '@tanzanite/wolfram-alpha';
import assert from 'assert/strict';
-import { ApplicationCommandOptionType, EmbedBuilder, type MessageOptions } from 'discord.js';
+import { ApplicationCommandOptionType, EmbedBuilder, type MessageCreateOptions } from 'discord.js';
assert(WolframAlphaAPI);
@@ -62,7 +62,7 @@ export default class WolframAlphaCommand extends BotCommand {
name: '📥 Input',
value: await this.client.utils.inspectCleanRedactCodeblock(args.expression)
});
- const sendOptions: MessageOptions = { content: null, allowedMentions: AllowedMentions.none() };
+ const sendOptions: MessageCreateOptions = { content: '', allowedMentions: AllowedMentions.none() };
try {
const calculated = await (args.image
? waApi.getSimple({ i: args.expression, timeout: 1, background: '2C2F33', foreground: 'white' })
diff --git a/src/context-menu-commands/message/viewRaw.ts b/src/context-menu-commands/message/viewRaw.ts
index 0a8fcfc..08a421d 100644
--- a/src/context-menu-commands/message/viewRaw.ts
+++ b/src/context-menu-commands/message/viewRaw.ts
@@ -1,4 +1,4 @@
-import { ContextMenuCommand } from 'discord-akairo';
+import { ContextMenuCommand } from '@notenoughupdates/discord-akairo';
import { ApplicationCommandType, type ContextMenuCommandInteraction, type Message } from 'discord.js';
import { getRawData } from '../../commands/utilities/viewRaw.js';
diff --git a/src/context-menu-commands/user/modlog.ts b/src/context-menu-commands/user/modlog.ts
index c78396e..b68a7e9 100644
--- a/src/context-menu-commands/user/modlog.ts
+++ b/src/context-menu-commands/user/modlog.ts
@@ -1,6 +1,6 @@
import { ModlogCommand } from '#commands';
import { emojis, SlashMessage } from '#lib';
-import { CommandUtil, ContextMenuCommand } from 'discord-akairo';
+import { CommandUtil, ContextMenuCommand } from '@notenoughupdates/discord-akairo';
import { ApplicationCommandType, type ContextMenuCommandInteraction } from 'discord.js';
export default class ModlogContextMenuCommand extends ContextMenuCommand {
@@ -8,7 +8,8 @@ export default class ModlogContextMenuCommand extends ContextMenuCommand {
super('modlog', {
name: "Users's Modlogs",
type: ApplicationCommandType.User,
- category: 'user'
+ category: 'user',
+ dmPermission: false
});
}
@@ -28,6 +29,8 @@ export default class ModlogContextMenuCommand extends ContextMenuCommand {
const pseudoMessage = new SlashMessage(this.client, interaction as any);
pseudoMessage.util = new CommandUtil(this.client.commandHandler, pseudoMessage);
- void new ModlogCommand().exec(pseudoMessage, { search: interaction.targetId, hidden: false });
+ const command = this.client.commandHandler.modules.get('modlog') as ModlogCommand;
+
+ void command.exec(pseudoMessage, { search: interaction.targetId, hidden: false });
}
}
diff --git a/src/context-menu-commands/user/userInfo.ts b/src/context-menu-commands/user/userInfo.ts
index 6d7f3b6..283e4a0 100644
--- a/src/context-menu-commands/user/userInfo.ts
+++ b/src/context-menu-commands/user/userInfo.ts
@@ -1,27 +1,35 @@
import { UserInfoCommand } from '#commands';
-import { format } from '#lib';
-import { ContextMenuCommand } from 'discord-akairo';
-import { ApplicationCommandType, type ContextMenuCommandInteraction, type Guild } from 'discord.js';
+import { emojis } from '#lib';
+import { ContextMenuCommand } from '@notenoughupdates/discord-akairo';
+import assert from 'assert';
+import { ApplicationCommandType, GuildMember, UserContextMenuCommandInteraction } from 'discord.js';
export default class UserInfoContextMenuCommand extends ContextMenuCommand {
public constructor() {
super('userInfo', {
name: 'User Info',
type: ApplicationCommandType.User,
- category: 'user'
+ category: 'user',
+ dmPermission: false
});
}
- public override async exec(interaction: ContextMenuCommandInteraction) {
+ public override async exec(interaction: UserContextMenuCommandInteraction) {
+ if (!interaction.inCachedGuild())
+ return interaction.reply({
+ content: `${emojis.error} You can't use this command outside of a server.`,
+ ephemeral: true
+ });
+
await interaction.deferReply({ ephemeral: true });
- const user = await this.client.users.fetch(interaction.targetId).catch(() => null);
- if (!user) return interaction.reply(`⁉ I couldn't find that user`);
+ const user = interaction.targetUser;
+
+ const guild = interaction.guild ?? undefined;
- const guild = interaction.guild as Guild;
+ const member = interaction.targetMember ?? undefined;
- const member = await guild.members.fetch(interaction.targetId).catch(() => null);
- if (!member) return interaction.reply(`${format.input(user.tag)} doesn't appear to be a member of this server anymore.`);
+ assert(member instanceof GuildMember || member === undefined);
const userEmbed = await UserInfoCommand.makeUserInfoEmbed(user, member, guild);
diff --git a/src/listeners/bush/appealListener.ts b/src/listeners/bush/appealListener.ts
index 46859d1..99f1505 100644
--- a/src/listeners/bush/appealListener.ts
+++ b/src/listeners/bush/appealListener.ts
@@ -2,7 +2,7 @@ import { BotListener, colors, Emitter, mappings, ModLog, type BotClientEvents }
import assert from 'assert/strict';
import { EmbedBuilder, Events } from 'discord.js';
import UserInfoCommand from '../../commands/info/userInfo.js';
-import ModlogCommand from '../../commands/moderation/modlog.js';
+import { generateModlogInfo, modlogSeparator } from '../../commands/moderation/modlog.js';
export default class AppealListener extends BotListener {
public constructor() {
@@ -42,7 +42,9 @@ export default class AppealListener extends BotListener {
await ModLog.findAll({
where: {
user: user.id,
- guild: message.guildId
+ guild: message.guildId,
+ pseudo: false,
+ hidden: false
},
order: [['createdAt', 'DESC']]
})
@@ -60,15 +62,19 @@ export default class AppealListener extends BotListener {
member: {
if (!message.guild.members.cache.has(user.id)) break member;
+
const member = message.guild.members.cache.get(user.id)!;
+
UserInfoCommand.generateServerInfoField(embed, member);
- if (member.roles.cache.size > 1) UserInfoCommand.generateRolesField(embed, member);
+ if (member.roles.cache.size > 1) {
+ UserInfoCommand.generateRolesField(embed, member);
+ }
}
embed.addFields({
name: '» Latest Modlogs',
value: latestModlogs.length
- ? latestModlogs.map((ml) => ModlogCommand.generateModlogInfo(ml, false, false)).join(ModlogCommand.separator)
+ ? latestModlogs.map((ml) => generateModlogInfo(ml, false, false)).join(modlogSeparator)
: 'No Modlogs Found'
});
diff --git a/src/listeners/bush/experimentYoink.ts b/src/listeners/bush/experimentYoink.ts
new file mode 100644
index 0000000..5b7e526
--- /dev/null
+++ b/src/listeners/bush/experimentYoink.ts
@@ -0,0 +1,28 @@
+import { BotClientEvents, BotListener, Emitter, mappings } from '#lib';
+import { Events, Routes } from 'discord.js';
+
+export default class ExperimentYoink extends BotListener {
+ public constructor() {
+ super('experimentYoink', {
+ emitter: Emitter.Client,
+ event: Events.MessageCreate
+ });
+ }
+
+ public async exec(...[message]: BotClientEvents[Events.MessageCreate]): Promise<any> {
+ if (message.channelId !== '1019830755658055691') return;
+ if (message.embeds.length < 1) return;
+ if (!message.embeds[0].title?.includes('Guild Experiment')) return;
+
+ const guild = this.client.guilds.cache.get(mappings.guilds["Moulberry's Bush"]);
+
+ if (guild == null) return;
+
+ return await this.client.rest.post(Routes.channelMessages('795356494261911553'), {
+ body: {
+ content: message.content,
+ embeds: message.embeds.map((embed) => embed.toJSON())
+ }
+ });
+ }
+}
diff --git a/src/listeners/client/ready.ts b/src/listeners/client/ready.ts
index b74c132..e19c4eb 100644
--- a/src/listeners/client/ready.ts
+++ b/src/listeners/client/ready.ts
@@ -1,6 +1,9 @@
import { BotClientEvents, BotListener, Emitter, Guild } from '#lib';
+import { commas } from '#lib/common/tags.js';
+import { humanizeDuration } from '@notenoughupdates/humanize-duration';
import chalk from 'chalk';
import { Events } from 'discord.js';
+import { performance } from 'perf_hooks';
export default class ReadyListener extends BotListener {
public constructor() {
@@ -12,19 +15,28 @@ export default class ReadyListener extends BotListener {
// eslint-disable-next-line no-empty-pattern
public async exec(...[]: BotClientEvents[Events.ClientReady]) {
+ performance.mark('clientReady');
+
process.emit('ready' as any);
const tag = `<<${this.client.user?.tag}>>`,
- guildCount = `<<${this.client.guilds.cache.size.toLocaleString()}>>`,
- userCount = `<<${this.client.users.cache.size.toLocaleString()}>>`;
+ guildCount = commas`<<${this.client.guilds.cache.size}>>`,
+ userCount = commas`<<${this.client.users.cache.size}>>`;
void this.client.logger.success('ready', `Logged in to ${tag} serving ${guildCount} guilds and ${userCount} users.`);
- console.log(
- chalk.blue(
- `------------------------------------------------------------------------------${
- this.client.config.isDevelopment ? '---' : this.client.config.isBeta ? '----' : ''
- }`
- )
+
+ console.log(chalk.blue('-'.repeat(84 + (this.client.config.isDevelopment ? 3 : this.client.config.isBeta ? 4 : 0))));
+
+ const measure = performance.measure('start', 'processStart', 'clientReady');
+
+ void this.client.logger.info(
+ 'ready',
+ `Took <<${humanizeDuration(measure.duration, {
+ language: 'en',
+ largest: 3,
+ round: false,
+ maxDecimalPoints: 3
+ })}>> to start.`
);
const guilds = await Guild.findAll();
diff --git a/src/listeners/commands/commandBlocked.ts b/src/listeners/commands/commandBlocked.ts
index c81857c..7795241 100644
--- a/src/listeners/commands/commandBlocked.ts
+++ b/src/listeners/commands/commandBlocked.ts
@@ -9,7 +9,7 @@ import {
type BotCommandHandlerEvents,
type CommandMessage
} from '#lib';
-import { type Client, type InteractionReplyOptions, type ReplyMessageOptions } from 'discord.js';
+import { type Client, type InteractionReplyOptions, type MessageReplyOptions } from 'discord.js';
export default class CommandBlockedListener extends BotListener {
public constructor() {
@@ -124,7 +124,7 @@ export default class CommandBlockedListener extends BotListener {
}
// some inhibitors do not have message.util yet
- function respond(content: string | (ReplyMessageOptions & InteractionReplyOptions)) {
+ function respond(content: string | (MessageReplyOptions & InteractionReplyOptions)) {
return message.util ? message.util.reply(content) : message.reply(content);
}
}
diff --git a/src/listeners/contextCommands/contextCommandBlocked.ts b/src/listeners/contextCommands/contextCommandBlocked.ts
index 93b53c7..80c7a34 100644
--- a/src/listeners/contextCommands/contextCommandBlocked.ts
+++ b/src/listeners/contextCommands/contextCommandBlocked.ts
@@ -1,6 +1,6 @@
import { BotListener, ContextCommandHandlerEvent, Emitter, emojis, format } from '#lib';
-import { type ContextMenuCommandHandlerEvents } from 'discord-akairo';
-import { BuiltInReasons } from 'discord-akairo/dist/src/util/Constants.js';
+import { type ContextMenuCommandHandlerEvents } from '@notenoughupdates/discord-akairo';
+import { BuiltInReasons } from '@notenoughupdates/discord-akairo/dist/src/util/Constants.js';
export default class ContextCommandBlockedListener extends BotListener {
public constructor() {
diff --git a/src/listeners/contextCommands/contextCommandError.ts b/src/listeners/contextCommands/contextCommandError.ts
index 24e5cef..5043bae 100644
--- a/src/listeners/contextCommands/contextCommandError.ts
+++ b/src/listeners/contextCommands/contextCommandError.ts
@@ -9,7 +9,7 @@ import {
getErrorStack,
IFuckedUpError
} from '#lib';
-import { type ContextMenuCommand, type ContextMenuCommandHandlerEvents } from 'discord-akairo';
+import { type ContextMenuCommand, type ContextMenuCommandHandlerEvents } from '@notenoughupdates/discord-akairo';
import { ChannelType, Client, ContextMenuCommandInteraction, EmbedBuilder, GuildTextBasedChannel } from 'discord.js';
export default class ContextCommandErrorListener extends BotListener {
diff --git a/src/listeners/contextCommands/contextCommandNotFound.ts b/src/listeners/contextCommands/contextCommandNotFound.ts
index da364ed..e7538ee 100644
--- a/src/listeners/contextCommands/contextCommandNotFound.ts
+++ b/src/listeners/contextCommands/contextCommandNotFound.ts
@@ -1,5 +1,5 @@
import { BotListener, ContextCommandHandlerEvent, Emitter } from '#lib';
-import { type ContextMenuCommandHandlerEvents } from 'discord-akairo';
+import { type ContextMenuCommandHandlerEvents } from '@notenoughupdates/discord-akairo';
export default class ContextCommandNotFoundListener extends BotListener {
public constructor() {
diff --git a/src/listeners/contextCommands/contextCommandStarted.ts b/src/listeners/contextCommands/contextCommandStarted.ts
index bf7cc58..a820d3e 100644
--- a/src/listeners/contextCommands/contextCommandStarted.ts
+++ b/src/listeners/contextCommands/contextCommandStarted.ts
@@ -1,5 +1,5 @@
import { BotListener, ContextCommandHandlerEvent, Emitter } from '#lib';
-import { ContextMenuCommandHandlerEvents } from 'discord-akairo';
+import { ContextMenuCommandHandlerEvents } from '@notenoughupdates/discord-akairo';
import { ApplicationCommandType, ChannelType } from 'discord.js';
export default class ContextCommandStartedListener extends BotListener {
diff --git a/src/listeners/guild/syncUnbanPunishmentModel.ts b/src/listeners/guild/syncUnbanPunishmentModel.ts
index eac3aa2..c6de768 100644
--- a/src/listeners/guild/syncUnbanPunishmentModel.ts
+++ b/src/listeners/guild/syncUnbanPunishmentModel.ts
@@ -13,7 +13,7 @@ export default class SyncUnbanListener extends BotListener {
where: {
user: ban.user.id,
guild: ban.guild.id,
- type: ActivePunishmentType.BAN
+ type: ActivePunishmentType.Ban
}
});
for (const dbBan of bans) {
diff --git a/src/listeners/interaction/$interactionCreate.ts b/src/listeners/interaction/$interactionCreate.ts
new file mode 100644
index 0000000..86aa5e2
--- /dev/null
+++ b/src/listeners/interaction/$interactionCreate.ts
@@ -0,0 +1,31 @@
+import { BotListener, Emitter, TanzaniteEvent, type BotClientEvents } from '#lib';
+import { Events, InteractionType } from 'discord.js';
+
+export default class InteractionCreateListener extends BotListener {
+ public constructor() {
+ super('interactionCreate', {
+ emitter: Emitter.Client,
+ event: Events.InteractionCreate
+ });
+ }
+
+ public async exec(...[interaction]: BotClientEvents[Events.InteractionCreate]) {
+ if (!interaction) return;
+
+ void this.client.console.verbose(
+ 'interactionVerbose',
+ `An interaction of type <<${InteractionType[interaction.type]}>> was received from <<${interaction.user.tag}>>.`
+ );
+
+ if (interaction.isCommand() || interaction.isAutocomplete()) {
+ // handled by the command handler
+ return;
+ } else if (interaction.isButton()) {
+ this.client.emit(TanzaniteEvent.Button, interaction);
+ } else if (interaction.isModalSubmit()) {
+ this.client.emit(TanzaniteEvent.ModalSubmit, interaction);
+ } else if (interaction.isSelectMenu()) {
+ this.client.emit(TanzaniteEvent.SelectMenu, interaction);
+ }
+ }
+}
diff --git a/src/listeners/interaction/button.ts b/src/listeners/interaction/button.ts
index e69de29..d0730d5 100644
--- a/src/listeners/interaction/button.ts
+++ b/src/listeners/interaction/button.ts
@@ -0,0 +1,129 @@
+import {
+ BotClientEvents,
+ BotListener,
+ Emitter,
+ emojis,
+ handleAppealAttempt,
+ handleAppealDecision,
+ handleAutomodInteraction,
+ TanzaniteEvent
+} from '#lib';
+import {
+ ActionRowData,
+ ButtonInteraction,
+ ComponentType,
+ ModalActionRowComponentData,
+ TextInputComponentData,
+ TextInputStyle
+} from 'discord.js';
+
+export default class ButtonListener extends BotListener {
+ public constructor() {
+ super(TanzaniteEvent.Button, {
+ emitter: Emitter.Client,
+ event: TanzaniteEvent.Button
+ });
+ }
+
+ public async exec(...[interaction]: BotClientEvents[TanzaniteEvent.Button]) {
+ const { customId } = interaction;
+
+ if (customId.startsWith('automod;')) {
+ return void handleAutomodInteraction(interaction);
+ } else if (customId.startsWith('button-role;')) {
+ return void this.handleButtonRoles(interaction);
+ } else if (customId === 'test;modal') {
+ return this.handleTestModal(interaction);
+ } else if (customId.startsWith('test;lots;') || customId.startsWith('test;button;')) {
+ return await interaction.reply({
+ content: 'Buttons go brrr',
+ ephemeral: true
+ });
+ } else if (customId.startsWith('appeal_attempt;')) {
+ return handleAppealAttempt(interaction);
+ } else if (customId.startsWith('appeal_accept;') || customId.startsWith('appeal_deny;')) {
+ return handleAppealDecision(interaction);
+ }
+ }
+
+ private async handleButtonRoles(interaction: ButtonInteraction) {
+ if (!interaction.inCachedGuild()) return;
+
+ const [, roleId] = interaction.customId.split(';');
+ const role = interaction.guild.roles.cache.get(roleId);
+ if (!role) {
+ return interaction.reply({
+ content: `${emojis.error} That role does not exist.`,
+ ephemeral: true
+ });
+ }
+ const has = interaction.member.roles.cache.has(roleId);
+ await interaction.deferReply({ ephemeral: true });
+ if (has) {
+ const success = await interaction.member.roles.remove(roleId).catch(() => false);
+ if (success) {
+ return interaction.editReply({
+ content: `${emojis.success} Removed the ${role} role from you.`,
+ allowedMentions: {}
+ });
+ } else {
+ return interaction.editReply({
+ content: `${emojis.error} Failed to remove ${role} from you.`,
+ allowedMentions: {}
+ });
+ }
+ } else {
+ const success = await interaction.member.roles.add(roleId).catch(() => false);
+ if (success) {
+ return interaction.editReply({
+ content: `${emojis.success} Added the ${role} role to you.`,
+ allowedMentions: {}
+ });
+ } else {
+ return interaction.editReply({
+ content: `${emojis.error} Failed to add ${role} to you.`,
+ allowedMentions: {}
+ });
+ }
+ }
+ }
+
+ private async handleTestModal(interaction: ButtonInteraction) {
+ const shortText = (
+ options: Pick<TextInputComponentData, 'customId' | 'label' | 'placeholder'> & Partial<TextInputComponentData>
+ ): ActionRowData<ModalActionRowComponentData> => ({
+ type: ComponentType.ActionRow as const,
+ components: [
+ {
+ type: ComponentType.TextInput as const,
+ style: TextInputStyle.Short as const,
+ ...options
+ }
+ ]
+ });
+
+ return interaction.showModal({
+ customId: 'test;login',
+ title: 'Login (real)',
+ components: [
+ shortText({
+ customId: 'test;login;email',
+ label: 'Email',
+ placeholder: 'Email'
+ }),
+ shortText({
+ customId: 'test;login;password',
+ label: 'Password',
+ placeholder: 'Password'
+ }),
+ shortText({
+ customId: 'test;login;2fa',
+ label: 'Enter Discord Auth Code',
+ minLength: 6,
+ maxLength: 6,
+ placeholder: '6-digit authentication code'
+ })
+ ]
+ });
+ }
+}
diff --git a/src/listeners/interaction/interactionCreate.ts b/src/listeners/interaction/interactionCreate.ts
deleted file mode 100644
index ced359c..0000000
--- a/src/listeners/interaction/interactionCreate.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import {
- BotListener,
- Emitter,
- emojis,
- format,
- formatList,
- handleAutomodInteraction,
- surroundEach,
- type BotClientEvents
-} from '#lib';
-import { Events, InteractionType } from 'discord.js';
-
-export default class InteractionCreateListener extends BotListener {
- public constructor() {
- super('interactionCreate', {
- emitter: Emitter.Client,
- event: Events.InteractionCreate
- });
- }
-
- public async exec(...[interaction]: BotClientEvents[Events.InteractionCreate]) {
- if (!interaction) return;
- if ('customId' in interaction && (interaction as any)['customId'].startsWith('test')) return;
- void this.client.console.verbose(
- 'interactionVerbose',
- `An interaction of type <<${InteractionType[interaction.type]}>> was received from <<${interaction.user.tag}>>.`
- );
- if (interaction.type === InteractionType.ApplicationCommand) {
- return;
- } else if (interaction.isButton()) {
- const id = interaction.customId;
- if (['paginate_', 'command_', 'confirmationPrompt_', 'appeal'].some((s) => id.startsWith(s))) return;
- else if (id.startsWith('automod;')) void handleAutomodInteraction(interaction);
- else if (id.startsWith('button-role;') && interaction.inCachedGuild()) {
- const [, roleId] = id.split(';');
- const role = interaction.guild.roles.cache.get(roleId);
- if (!role) return interaction.reply({ content: `${emojis.error} That role does not exist.`, ephemeral: true });
- const has = interaction.member.roles.cache.has(roleId);
- await interaction.deferReply({ ephemeral: true });
- if (has) {
- const success = await interaction.member.roles.remove(roleId).catch(() => false);
- if (success)
- return interaction.editReply({
- content: `${emojis.success} Removed the ${role} role from you.`,
- allowedMentions: {}
- });
- else
- return interaction.editReply({
- content: `${emojis.error} Failed to remove ${role} from you.`,
- allowedMentions: {}
- });
- } else {
- const success = await interaction.member.roles.add(roleId).catch(() => false);
- if (success)
- return interaction.editReply({
- content: `${emojis.success} Added the ${role} role to you.`,
- allowedMentions: {}
- });
- else
- return interaction.editReply({
- content: `${emojis.error} Failed to add ${role} to you.`,
- allowedMentions: {}
- });
- }
- } else return await interaction.reply({ content: 'Buttons go brrr', ephemeral: true });
- } else if (interaction.isSelectMenu()) {
- if (interaction.customId.startsWith('command_')) return;
- return await interaction.reply({
- content: `You selected ${
- Array.isArray(interaction.values)
- ? formatList(surroundEach(interaction.values, '`'), 'and')
- : format.input(interaction.values)
- }.`,
- ephemeral: true
- });
- }
- }
-}
diff --git a/src/listeners/interaction/modalSubmit.ts b/src/listeners/interaction/modalSubmit.ts
new file mode 100644
index 0000000..8cf93f4
--- /dev/null
+++ b/src/listeners/interaction/modalSubmit.ts
@@ -0,0 +1,20 @@
+import { BotClientEvents, BotListener, Emitter, emojis, handleAppealSubmit, TanzaniteEvent } from '#lib';
+
+export default class ModalSubmitListener extends BotListener {
+ public constructor() {
+ super(TanzaniteEvent.ModalSubmit, {
+ emitter: Emitter.Client,
+ event: TanzaniteEvent.ModalSubmit
+ });
+ }
+
+ public async exec(...[interaction]: BotClientEvents[TanzaniteEvent.ModalSubmit]) {
+ const { customId } = interaction;
+
+ if (customId === 'test;login') {
+ return interaction.reply({ content: `${emojis.loading} Selling your account information to Facebook...`, ephemeral: true });
+ } else if (customId.startsWith('appeal_submit;')) {
+ return handleAppealSubmit(interaction);
+ }
+ }
+}
diff --git a/src/listeners/interaction/selectMenu.ts b/src/listeners/interaction/selectMenu.ts
new file mode 100644
index 0000000..112f303
--- /dev/null
+++ b/src/listeners/interaction/selectMenu.ts
@@ -0,0 +1,23 @@
+import { BotClientEvents, BotListener, Emitter, format, formatList, surroundEach, TanzaniteEvent } from '#lib';
+
+export default class SelectMenuListener extends BotListener {
+ public constructor() {
+ super(TanzaniteEvent.SelectMenu, {
+ emitter: Emitter.Client,
+ event: TanzaniteEvent.SelectMenu
+ });
+ }
+
+ public async exec(...[interaction]: BotClientEvents[TanzaniteEvent.SelectMenu]) {
+ if (interaction.customId.startsWith('command_')) return;
+
+ return await interaction.reply({
+ content: `You selected ${
+ Array.isArray(interaction.values)
+ ? formatList(surroundEach(interaction.values, '`'), 'and')
+ : format.input(interaction.values)
+ }.`,
+ ephemeral: true
+ });
+ }
+}
diff --git a/src/listeners/member-custom/levelUpdate.ts b/src/listeners/member-custom/levelUpdate.ts
index 53734fd..7d3cee7 100644
--- a/src/listeners/member-custom/levelUpdate.ts
+++ b/src/listeners/member-custom/levelUpdate.ts
@@ -29,11 +29,12 @@ export default class LevelUpdateListener extends BotListener {
.send(`${format.input(member.user.tag)} leveled up to level ${format.input(`${newLevel}`)}.`)
.catch(() => null);
- if (!success)
+ if (!success) {
await message.guild.error(
'LevelUpdate',
`Could not send level up message for ${member.user.tag} in <#${message.channel.id}>.`
);
+ }
}
private async assignLevelRoles(member: Args[0], newLevel: Args[2], message: Args[4]) {
diff --git a/src/listeners/message/autoPublisher.ts b/src/listeners/message/autoPublisher.ts
index 36c448a..46577e4 100644
--- a/src/listeners/message/autoPublisher.ts
+++ b/src/listeners/message/autoPublisher.ts
@@ -13,7 +13,10 @@ export default class autoPublisherListener extends BotListener {
if (!message.guild || !(await message.guild.hasFeature('autoPublish'))) return;
const autoPublishChannels = await message.guild.getSetting('autoPublishChannels');
if (autoPublishChannels) {
- if (message.channel.type === ChannelType.GuildNews && autoPublishChannels.some((x) => message.channel.id.includes(x))) {
+ if (
+ message.channel.type === ChannelType.GuildAnnouncement &&
+ autoPublishChannels.some((x) => message.channel.id.includes(x))
+ ) {
await message
.crosspost()
.then(
diff --git a/src/listeners/message/level.ts b/src/listeners/message/level.ts
index a445d1e..60742ab 100644
--- a/src/listeners/message/level.ts
+++ b/src/listeners/message/level.ts
@@ -3,6 +3,7 @@ import { MessageType } from 'discord.js';
export default class LevelListener extends BotListener {
#levelCooldowns: Set<string> = new Set();
+
public constructor() {
super('level', {
emitter: Emitter.CommandHandler,
@@ -14,34 +15,52 @@ export default class LevelListener extends BotListener {
public async exec(...[message]: BotCommandHandlerEvents[CommandHandlerEvent.MessageInvalid]) {
if (message.author.bot || !message.author || !message.inGuild()) return;
if (!(await message.guild.hasFeature('leveling'))) return;
- if (this.#levelCooldowns.has(`${message.guildId}-${message.author.id}`)) return;
+
+ const lock = `${message.guildId}-${message.author.id}`;
+ if (this.#levelCooldowns.has(lock)) return;
if ((await message.guild.getSetting('noXpChannels')).includes(message.channel.id)) return;
- if (message.type !== MessageType.Default && message.type !== MessageType.Reply) return; //checks for join messages, slash commands, booster messages etc
- const [user] = await Level.findOrBuild({
+
+ // checks for join messages, slash commands, booster messages etc
+ if (![MessageType.Default, MessageType.Reply, MessageType.ThreadStarterMessage].includes(message.type)) {
+ return;
+ }
+
+ const [levelEntry] = await Level.findOrBuild({
where: {
user: message.author.id,
guild: message.guildId
- },
- defaults: {
- user: message.author.id,
- guild: message.guildId,
- xp: 0
}
});
- const previousLevel = Level.convertXpToLevel(user.xp);
+
+ const previousLevel = Level.convertXpToLevel(levelEntry.xp);
const xpToGive = Level.genRandomizedXp();
- user.xp = user.xp + xpToGive;
- const success = await user.save().catch((e) => {
- void this.client.utils.handleError('level', e);
+
+ let xp = levelEntry.xp + xpToGive;
+
+ if (xp > Level.MAX_XP) {
+ xp = Level.MAX_XP;
+ }
+
+ const success = await levelEntry.update({ xp, user: message.author.id, guild: message.guild.id }).catch((e) => {
+ void this.client.utils.handleError('LevelListener', e);
+
return false;
});
- const newLevel = Level.convertXpToLevel(user.xp);
- if (previousLevel !== newLevel)
- this.client.emit(TanzaniteEvent.LevelUpdate, message.member!, previousLevel, newLevel, user.xp, message);
- if (success)
- void this.client.logger.verbose(`level`, `Gave <<${xpToGive}>> XP to <<${message.author.tag}>> in <<${message.guild}>>.`);
- this.#levelCooldowns.add(`${message.guildId}-${message.author.id}`);
- setTimeout(() => this.#levelCooldowns.delete(`${message.guildId}-${message.author.id}`), 60_000);
+
+ const newLevel = Level.convertXpToLevel(levelEntry.xp);
+
+ if (success) {
+ if (previousLevel !== newLevel) {
+ // level up messages and level roles
+ this.client.emit(TanzaniteEvent.LevelUpdate, message.member!, previousLevel, newLevel, levelEntry.xp, message);
+ }
+
+ void this.client.logger.verbose(`level`, `Gave <<${xpToGive}>> xp to <<${message.author.tag}>> in <<${message.guild}>>.`);
+ }
+
+ this.#levelCooldowns.add(lock);
+
+ setTimeout(() => this.#levelCooldowns.delete(lock), 60_000);
}
}
diff --git a/src/listeners/message/quoteCreate.ts b/src/listeners/message/quoteCreate.ts
index 1c3130c..fca6c9f 100644
--- a/src/listeners/message/quoteCreate.ts
+++ b/src/listeners/message/quoteCreate.ts
@@ -10,7 +10,7 @@ export default class QuoteCreateListener extends BotListener {
}
public async exec(...[message]: BotClientEvents[Events.MessageCreate]) {
- if (message.author.id !== mappings.users['IRONM00N'] || !this.client.config.isProduction) return;
+ if (message.author.id !== mappings.users['IRONM00N'] /* || !this.client.config.isProduction */) return;
if (!message.inGuild()) return;
const messages = await this.client.utils.resolveMessagesFromLinks(message.content);
diff --git a/src/listeners/track-manual-punishments/modlogSyncBan.ts b/src/listeners/track-manual-punishments/modlogSyncBan.ts
index 83f6dd4..5a4d768 100644
--- a/src/listeners/track-manual-punishments/modlogSyncBan.ts
+++ b/src/listeners/track-manual-punishments/modlogSyncBan.ts
@@ -37,7 +37,7 @@ export default class ModlogSyncBanListener extends BotListener {
const { log } = await Moderation.createModLogEntry({
client: this.client,
- type: ModLogType.PERM_BAN,
+ type: ModLogType.PermBan,
user: ban.user,
moderator: first.executor,
reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`,
diff --git a/src/listeners/track-manual-punishments/modlogSyncKick.ts b/src/listeners/track-manual-punishments/modlogSyncKick.ts
index e8b2433..a05e666 100644
--- a/src/listeners/track-manual-punishments/modlogSyncKick.ts
+++ b/src/listeners/track-manual-punishments/modlogSyncKick.ts
@@ -37,7 +37,7 @@ export default class ModlogSyncKickListener extends BotListener {
const { log } = await Moderation.createModLogEntry({
client: this.client,
- type: ModLogType.KICK,
+ type: ModLogType.Kick,
user: member.user,
moderator: first.executor,
reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`,
diff --git a/src/listeners/track-manual-punishments/modlogSyncTimeout.ts b/src/listeners/track-manual-punishments/modlogSyncTimeout.ts
index 2a4e0bb..f6abbab 100644
--- a/src/listeners/track-manual-punishments/modlogSyncTimeout.ts
+++ b/src/listeners/track-manual-punishments/modlogSyncTimeout.ts
@@ -41,7 +41,7 @@ export default class ModlogSyncTimeoutListener extends BotListener {
const { log } = await Moderation.createModLogEntry({
client: this.client,
- type: newTime ? ModLogType.TIMEOUT : ModLogType.REMOVE_TIMEOUT,
+ type: newTime ? ModLogType.Timeout : ModLogType.RemoveTimeout,
user: newMember.user,
moderator: first.executor,
reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`,
diff --git a/src/listeners/track-manual-punishments/modlogSyncUnban.ts b/src/listeners/track-manual-punishments/modlogSyncUnban.ts
index 4738066..35ee3d2 100644
--- a/src/listeners/track-manual-punishments/modlogSyncUnban.ts
+++ b/src/listeners/track-manual-punishments/modlogSyncUnban.ts
@@ -36,7 +36,7 @@ export default class ModlogSyncUnbanListener extends BotListener {
const { log } = await Moderation.createModLogEntry({
client: this.client,
- type: ModLogType.UNBAN,
+ type: ModLogType.Unban,
user: ban.user,
moderator: first.executor,
reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`,
diff --git a/src/listeners/ws/INTERACTION_CREATE.ts b/src/listeners/ws/INTERACTION_CREATE.ts
index d0327df..49f8bdf 100644
--- a/src/listeners/ws/INTERACTION_CREATE.ts
+++ b/src/listeners/ws/INTERACTION_CREATE.ts
@@ -1,25 +1,5 @@
-import { BotListener, capitalize, colors, Emitter, emojis, Moderation, PunishmentTypePresent } from '#lib';
-import assert from 'assert/strict';
-import {
- ActionRowBuilder,
- ButtonBuilder,
- ButtonStyle,
- ComponentType,
- EmbedBuilder,
- GatewayDispatchEvents,
- InteractionResponseType,
- InteractionType,
- Routes,
- Snowflake,
- TextInputStyle,
- User,
- type APIEmbed,
- type APIInteraction,
- type APIInteractionResponseChannelMessageWithSource,
- type APIInteractionResponseDeferredMessageUpdate,
- type APIInteractionResponseUpdateMessage,
- type APIModalInteractionResponse
-} from 'discord.js';
+import { BotListener, Emitter } from '#lib';
+import { GatewayDispatchEvents, type APIInteraction } from 'discord.js';
export default class WsInteractionCreateListener extends BotListener {
public constructor() {
@@ -29,215 +9,5 @@ export default class WsInteractionCreateListener extends BotListener {
});
}
- public async exec(interaction: APIInteraction) {
- // console.dir(interaction);
-
- const respond = (
- options:
- | APIModalInteractionResponse
- | APIInteractionResponseDeferredMessageUpdate
- | APIInteractionResponseChannelMessageWithSource
- | APIInteractionResponseUpdateMessage
- ) => {
- return this.client.rest.post(
- Routes.interactionCallback(interaction.id, interaction.token),
- options ? { body: options } : undefined
- );
- };
-
- const deferredMessageUpdate = () => {
- return respond({
- type: InteractionResponseType.DeferredMessageUpdate
- });
- };
-
- if (interaction.type === InteractionType.MessageComponent) {
- if (interaction.data.custom_id.startsWith('appeal;')) {
- const [, punishment, guildId, userId, modlogCase] = interaction.data.custom_id.split(';') as [
- 'appeal',
- PunishmentTypePresent,
- Snowflake,
- Snowflake,
- string
- ];
-
- const guild = this.client.guilds.resolve(guildId);
- if (!guild)
- return respond({
- type: InteractionResponseType.ChannelMessageWithSource,
- data: {
- content: `${emojis.error} I am no longer in that server.`
- }
- });
-
- const modal: APIModalInteractionResponse = {
- type: InteractionResponseType.Modal,
- data: {
- custom_id: `appeal_submit;${punishment};${guildId};${userId};${modlogCase}`,
- title: `${capitalize(punishment)} Appeal`,
- components: [
- {
- type: ComponentType.ActionRow,
- components: [
- {
- type: ComponentType.TextInput,
- style: TextInputStyle.Paragraph,
- max_length: 1024,
- required: true,
- label: `Why were you ${Moderation.punishmentToPastTense(punishment)}?`,
- placeholder: `Why do you think you received a ${punishment}?`,
- custom_id: 'appeal_reason'
- }
- ]
- },
- {
- type: ComponentType.ActionRow,
- components: [
- {
- type: ComponentType.TextInput,
- style: TextInputStyle.Paragraph,
- max_length: 1024,
- required: true,
- label: 'Do you believe it was fair?',
- placeholder: `Why do you think you received a ${punishment}?`,
- custom_id: 'appeal_fair'
- }
- ]
- },
- {
- type: ComponentType.ActionRow,
- components: [
- {
- type: ComponentType.TextInput,
- style: TextInputStyle.Paragraph,
- max_length: 1024,
- required: true,
- label: `Why should your ${punishment} be removed?`,
- placeholder: `Why should your ${punishment} be removed?`,
- custom_id: 'appeal_why'
- }
- ]
- }
- ]
- }
- };
-
- return respond(modal);
- } else if (
- interaction.data.custom_id.startsWith('appeal_accept;') ||
- interaction.data.custom_id.startsWith('appeal_deny;')
- ) {
- const [action, punishment, guildId, userId /* modlogCase */] = interaction.data.custom_id.split(';') as [
- 'appeal_accept' | 'appeal_deny',
- PunishmentTypePresent,
- Snowflake,
- Snowflake,
- string
- ];
-
- if (action === 'appeal_deny') {
- await this.client.users
- .send(userId, `Your ${punishment} appeal has been denied in ${this.client.guilds.resolve(guildId)!}.`)
- .catch(() => {});
-
- void respond({
- type: InteractionResponseType.ChannelMessageWithSource,
- data: {
- components: [
- {
- type: 1,
- components: [
- {
- type: ComponentType.Button,
- style: ButtonStyle.Danger,
- label: 'Close',
- custom_id: 'appeal_denied'
- }
- ]
- }
- ]
- }
- });
- }
- }
- } else if (interaction.type === InteractionType.ModalSubmit) {
- if (interaction.data.custom_id.startsWith('appeal_submit;')) {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [, punishment, guildId, userId, modlogCase] = interaction.data.custom_id.split(';') as [
- 'appeal_submit',
- PunishmentTypePresent,
- Snowflake,
- Snowflake,
- string
- ];
-
- const guild = this.client.guilds.resolve(guildId);
- if (!guild)
- return respond({
- type: InteractionResponseType.ChannelMessageWithSource,
- data: {
- content: `${emojis.error} I am no longer in that server.`
- }
- });
-
- const channel = await guild.getLogChannel('appeals');
- if (!channel)
- return respond({
- type: InteractionResponseType.ChannelMessageWithSource,
- data: {
- content: `${emojis.error} ${guild.name} has misconfigured their appeals channel.`
- }
- });
-
- assert(interaction.user);
- const user = new User(this.client, interaction.user);
- assert(user);
-
- // const caseId = await ModLog.findOne({ where: { user: userId, guild: guildId, id: modlogCase } });
-
- const embed = new EmbedBuilder()
- .setTitle(`${capitalize(punishment)} Appeal`)
- .setColor(colors.newBlurple)
- .setTimestamp()
- .setFooter({ text: `CaseID: ${modlogCase}` })
- .setAuthor({ name: user.tag, iconURL: user.displayAvatarURL() })
- .addFields(
- {
- name: `Why were you ${Moderation.punishmentToPastTense(punishment)}?`,
- value: interaction.data.components![0].components[0]!.value.substring(0, 1024)
- },
- {
- name: 'Do you believe it was fair?',
- value: interaction.data.components![1].components[0]!.value.substring(0, 1024)
- },
- {
- name: `Why should your ${punishment} be removed?`,
- value: interaction.data.components![2].components[0]!.value.substring(0, 1024)
- }
- )
- .toJSON() as APIEmbed;
-
- const components = [
- new ActionRowBuilder<ButtonBuilder>({
- components: [
- new ButtonBuilder({
- customId: `appeal_accept;${punishment};${guildId};${userId};${modlogCase}`,
- label: 'Accept',
- style: ButtonStyle.Success
- }).toJSON(),
- new ButtonBuilder({
- customId: `appeal_deny;${punishment};${guildId};${userId};${modlogCase}`,
- label: 'Deny',
- style: ButtonStyle.Danger
- }).toJSON()
- ]
- })
- ];
-
- await channel.send({ embeds: [embed], components });
- } else {
- return deferredMessageUpdate();
- }
- }
- }
+ public async exec(interaction: APIInteraction) {}
}
diff --git a/src/tasks/feature/removeExpiredPunishements.ts b/src/tasks/feature/removeExpiredPunishements.ts
index eac325a..5d5d5ab 100644
--- a/src/tasks/feature/removeExpiredPunishements.ts
+++ b/src/tasks/feature/removeExpiredPunishements.ts
@@ -35,7 +35,7 @@ export default class RemoveExpiredPunishmentsTask extends BotTask {
assert(guild);
switch (entry.type) {
- case ActivePunishmentType.BAN: {
+ case ActivePunishmentType.Ban: {
assert(user);
const result = await guild.customUnban({ user: user, reason: 'Punishment expired' });
if (['success', 'user not banned', 'cannot resolve user'].includes(result)) await entry.destroy();
@@ -43,7 +43,7 @@ export default class RemoveExpiredPunishmentsTask extends BotTask {
void this.client.logger.verbose(`removeExpiredPunishments`, `Unbanned ${entry.user}.`);
break;
}
- case ActivePunishmentType.BLOCK: {
+ case ActivePunishmentType.Block: {
if (!member) {
await entry.destroy(); // channel overrides are removed when the member leaves the guild
return;
@@ -54,7 +54,7 @@ export default class RemoveExpiredPunishmentsTask extends BotTask {
void this.client.logger.verbose(`removeExpiredPunishments`, `Unblocked ${entry.user}.`);
break;
}
- case ActivePunishmentType.MUTE: {
+ case ActivePunishmentType.Mute: {
if (!member) return;
const result = await member.customUnmute({ reason: 'Punishment expired' });
if (['success', 'failed to dm'].includes(result)) await entry.destroy();
@@ -62,7 +62,7 @@ export default class RemoveExpiredPunishmentsTask extends BotTask {
void this.client.logger.verbose(`removeExpiredPunishments`, `Unmuted ${entry.user}.`);
break;
}
- case ActivePunishmentType.ROLE: {
+ case ActivePunishmentType.Role: {
if (!member) return;
const role = guild?.roles?.cache?.get(entry.extraInfo);
if (!role) throw new Error(`Cannot unmute ${member.user.tag} because I cannot find the mute role.`);