Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/commands/ban.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EmbedBuilder } from 'discord.js';
import { EmbedBuilder, InteractionContextType } from 'discord.js';
import { SlashCommandBuilder } from '@discordjs/builders';
import { Ban } from '@/models/bans';
import { banMessageDeleteChoices, sendEventLogMessage, canActOnUserList, createNoPermissionEmbed } from '@/util';
Expand Down Expand Up @@ -148,6 +148,7 @@ const command = new SlashCommandBuilder()
.setDefaultMemberPermissions('0')
.setName('ban')
.setDescription('Ban user(s)')
.setContexts([InteractionContextType.Guild])
.addSubcommand(subcommand =>
subcommand.setName('user')
.setDescription('Ban a single user')
Expand Down
3 changes: 2 additions & 1 deletion src/commands/kick.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EmbedBuilder } from 'discord.js';
import { EmbedBuilder, InteractionContextType } from 'discord.js';
import { SlashCommandBuilder } from '@discordjs/builders';
import { Kick } from '@/models/kicks';
import { Ban } from '@/models/bans';
Expand Down Expand Up @@ -206,6 +206,7 @@ const command = new SlashCommandBuilder()
.setDefaultMemberPermissions('0')
.setName('kick')
.setDescription('Kick user(s)')
.setContexts([InteractionContextType.Guild])
.addSubcommand(subcommand =>
subcommand.setName('user')
.setDescription('Kick user')
Expand Down
7 changes: 5 additions & 2 deletions src/commands/list-warns.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EmbedBuilder } from 'discord.js';
import { EmbedBuilder, InteractionContextType } from 'discord.js';
import { SlashCommandBuilder } from '@discordjs/builders';
import { Warning } from '@/models/warnings';
import type { ChatInputCommandInteraction } from 'discord.js';
Expand Down Expand Up @@ -29,9 +29,11 @@ export async function listWarnsCommandHandler(interaction: ChatInputCommandInter
warningListEmbed.setDescription(`Showing warnings for <@${member.user.id}>\nTotal warnings: ${activeWarnings.length}`);

userWarnings.forEach((warn) => {
const t = Math.floor(warn.content.timestamp.getTime() / 1000);

warningListEmbed.addFields({
name: `Warning ID: \`${warn.content.id}\`` + (warn.isExpired ? ' - EXPIRED' : ''),
value: warn.content.reason + `\n---\nGiven on ${warn.content.timestamp.toLocaleDateString()} by <@${warn.content.admin_user_id}>`
value: warn.content.reason + `\n---\nGiven at <t:${t}:S> by <@${warn.content.admin_user_id}>`
});
});

Expand All @@ -42,6 +44,7 @@ const command = new SlashCommandBuilder()
.setDefaultMemberPermissions('0')
.setName('list-warns')
.setDescription('View a user\'s warns')
.setContexts([InteractionContextType.Guild])
.addUserOption((option) => {
return option.setName('user')
.setDescription('User whose warn history will be displayed')
Expand Down
2 changes: 2 additions & 0 deletions src/commands/modping.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { InteractionContextType } from 'discord.js';
import { SlashCommandBuilder } from '@discordjs/builders';
import { ModPingSettings } from '@/models/modPingSettings';
import { getRoleFromSettings } from '@/util';
Expand Down Expand Up @@ -138,6 +139,7 @@ const command = new SlashCommandBuilder();
command.setDefaultMemberPermissions('0');
command.setName('mod-ping');
command.setDescription('Manage your @Mod-Ping role.');
command.setContexts([InteractionContextType.Guild]);
command.addSubcommand(cmd =>
cmd.setName('toggle')
.setDescription('Manually toggle @Mod-Ping. (Overrides auto-assign, but still auto-assigns when your status changes)')
Expand Down
2 changes: 2 additions & 0 deletions src/commands/remove-warn.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SlashCommandBuilder } from '@discordjs/builders';
import { InteractionContextType } from 'discord.js';
import { Warning } from '@/models/warnings';
import type { ChatInputCommandInteraction } from 'discord.js';

Expand Down Expand Up @@ -39,6 +40,7 @@ const command = new SlashCommandBuilder()
.setDefaultMemberPermissions('0')
.setName('remove-warn')
.setDescription('Remove a warn')
.setContexts([InteractionContextType.Guild])
.addStringOption((option) => {
return option.setName('id')
.setDescription('ID of the warn to remove')
Expand Down
2 changes: 2 additions & 0 deletions src/commands/settings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { escapeMarkdown, SlashCommandBuilder } from '@discordjs/builders';
import { ZodError } from 'zod';
import { InteractionContextType } from 'discord.js';
import { getAllSettings, getSetting, setSetting, settingsDefinitions } from '@/models/settings';
import type { SettingsKeys } from '@/models/settings';
import type { ChatInputCommandInteraction } from 'discord.js';
Expand Down Expand Up @@ -138,6 +139,7 @@ const command = new SlashCommandBuilder();
command.setDefaultMemberPermissions('0');
command.setName('settings');
command.setDescription('Setup the bot');
command.setContexts([InteractionContextType.Guild]);
command.addSubcommand((cmd) => {
cmd.setName('set');
cmd.setDescription('Change a settings key');
Expand Down
3 changes: 2 additions & 1 deletion src/commands/slow-mode.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SlashCommandBuilder } from '@discordjs/builders';
import { ChannelType, EmbedBuilder } from 'discord.js';
import { ChannelType, EmbedBuilder, InteractionContextType } from 'discord.js';
import { SlowMode, SlowModeStage } from '@/models/slow-mode';
import handleSlowMode from '@/slow-mode';
import { sendEventLogMessage } from '@/util';
Expand Down Expand Up @@ -539,6 +539,7 @@ const command = new SlashCommandBuilder()
.setDefaultMemberPermissions('0')
.setName('slow-mode')
.setDescription('Configure slow mode for a channel')
.setContexts([InteractionContextType.Guild])
.addSubcommandGroup((group) => {
group.setName('enable')
.setDescription('Enable slow mode for a channel')
Expand Down
101 changes: 101 additions & 0 deletions src/commands/user-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { ApplicationIntegrationType, EmbedBuilder, InteractionContextType } from 'discord.js';
import { SlashCommandBuilder } from '@discordjs/builders';
import { User } from '@/models/users';
import { Warning } from '@/models/warnings';
import { getSetting } from '@/models/settings';
import type { ChatInputCommandInteraction, GuildMemberRoleManager } from 'discord.js';

export async function userInfoCommandHandler(interaction: ChatInputCommandInteraction): Promise<void> {
await interaction.deferReply({
ephemeral: true
});

const member = interaction.member;
const user = interaction.user;
const [dbUser] = await User.findOrCreate({
where: {
user_id: user.id
}
});
const inDMs = !member || interaction.context === InteractionContextType.BotDM;

const { rows } = await Warning.findAndCountAll({
where: {
user_id: user.id
}
});
const userWarnings = rows.map(v => ({
content: v,
isExpired: v.expires_at && v.expires_at < new Date()
}));
const activeWarnings = userWarnings.filter(v => !v.isExpired);

const levelingEnabled = await getSetting('leveling.enabled');
const minimumXP = await getSetting('leveling.xp-required-for-trusted');
const daysRequiredForTrusted = await getSetting('leveling.days-required-for-trusted');
const messageXP = await getSetting('leveling.message-xp');
const messageTimeout = await getSetting('leveling.message-timeout-seconds');
const trustedRole = await getSetting('role.trusted');
const untrustedRole = await getSetting('role.untrusted');

const now = new Date();

const userInfoEmbed = new EmbedBuilder();
userInfoEmbed.setTitle('User info');
userInfoEmbed.setColor(0x9D6FF3);
userInfoEmbed.setAuthor({ name: user.username, iconURL: interaction.user.displayAvatarURL() });

let userInfoDesc = '';
const _warningsText = activeWarnings.length > 0 ? `**<:mod:1462244578118729952> Warnings (${activeWarnings.length}):**\n` : '**<:mod:1462244578118729952> No warnings.**';

if (inDMs) {
userInfoDesc = _warningsText;
userInfoEmbed.setFooter({ text: 'Note: to see your current XP, run this command in the server.' });
} else if (!levelingEnabled || !trustedRole) {
userInfoDesc = _warningsText;
} else if (untrustedRole && (member.roles as GuildMemberRoleManager).cache.has(untrustedRole)) {
userInfoDesc = `You have <@&${untrustedRole}>. You cannot earn XP.\n\n${_warningsText}`;
} else if ((member.roles as GuildMemberRoleManager).cache.has(trustedRole)) {
userInfoDesc = `<:trusted:1462263739670728798> You have <@&${trustedRole}>.\n\n${_warningsText}`;
} else if (dbUser.trusted_time_start_date) {
let seconds = Math.floor((now.getTime() - dbUser.trusted_time_start_date.getTime()) / 1000);

const days = Math.floor(seconds / 86400);
seconds -= days * 86400;

const hours = Math.floor(seconds / 3600) % 24;
seconds -= hours * 3600;

const minutes = Math.floor(seconds / 60) % 60;
seconds -= minutes * 60;

userInfoDesc = `You currently don't have <@&${trustedRole}>. You must meet both requirements:\n\n${dbUser.xp < minimumXP ? '<:disallow:1462263736902488064>' : '<:allow:1462263738353586197>'} **${dbUser.xp}** / ${minimumXP} XP\n-# (${messageXP} XP / msg, ${messageTimeout}s cooldown between msgs, some channels blacklisted)\n${Math.floor((now.getTime() - dbUser.trusted_time_start_date.getTime()) / 1000) < daysRequiredForTrusted * 24 * 60 * 60 ? '<:disallow:1462263736902488064>' : '<:allow:1462263738353586197>'} In server for **${days}d ${hours}h ${minutes}m** / ${daysRequiredForTrusted} days\n-# (after joining / having Trusted removed)\n\n${_warningsText}`;
} else {
userInfoDesc = `You have not interacted with the community yet.\n\n${_warningsText}`;
}

userInfoEmbed.setDescription(userInfoDesc);

activeWarnings.forEach((warn) => {
const t = Math.floor(warn.content.timestamp.getTime() / 1000);

userInfoEmbed.addFields({
name: `<t:${t}:S> (ID: \`${warn.content.id}\`):`,
value: warn.content.reason
});
});

await interaction.followUp({ embeds: [userInfoEmbed], ephemeral: true });
}

const command = new SlashCommandBuilder()
.setName('user-info')
.setContexts([InteractionContextType.BotDM, InteractionContextType.Guild])
.setIntegrationTypes([ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall])
.setDescription('View moderation info about yourself');

export default {
name: command.name,
handler: userInfoCommandHandler,
deploy: command.toJSON()
};
7 changes: 5 additions & 2 deletions src/commands/warn.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { EmbedBuilder } from 'discord.js';
import { EmbedBuilder, InteractionContextType } from 'discord.js';
import { SlashCommandBuilder } from '@discordjs/builders';
import { Op } from 'sequelize';
import { Warning } from '@/models/warnings';
Expand Down Expand Up @@ -183,14 +183,16 @@ export async function warnHandler(interaction: CommandInteraction | ModalSubmitI
for (let i = 0; i < rows.length; i++) {
const warning = rows[i];

const t = warning.timestamp.getTime() / 1000;

pastWarningsEmbed.addFields(
{
name: `${ordinal(i + 1)} Warning`,
value: warning.reason
},
{
name: 'Date',
value: warning.timestamp.toLocaleDateString(),
value: `<t:${t}:d> <t:${t}:t>`,
inline: true
}
);
Expand Down Expand Up @@ -276,6 +278,7 @@ const command = new SlashCommandBuilder()
.setDefaultMemberPermissions('0')
.setName('warn')
.setDescription('Warn user(s)')
.setContexts([InteractionContextType.Guild])
.addSubcommand((subcommand) => {
return subcommand.setName('user')
.setDescription('Warn a user')
Expand Down
2 changes: 1 addition & 1 deletion src/events/messageCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default async function messageCreateHandler(message: Message): Promise<vo
}

if (!message.guild) {
message.reply('Hello! These DMs are __not__ monitored.\n\nIf you wish to contact Pretendo\'s mod team, please read the contents of https://discord.com/channels/408718485913468928/1370584407261581392, then create a modmail ticket for your issue.\n\nIf you want to submit a Network appeal/report or Discord ban appeal, please do so on the **[Forum](<https://forum.pretendo.network/>)**.');
message.reply('Hello! These DMs are __not__ monitored.\n\nIf you wish to contact Pretendo\'s mod team, please read the contents of https://discord.com/channels/408718485913468928/1370584407261581392, then create a modmail ticket for your issue.\n\nIf you want to submit a Network appeal/report or Discord ban appeal, please do so on the **[Forum](<https://forum.pretendo.network/>)**.\n\nTo view your warns, run Chubby\'s `/user-info` command.');
} else {
if (await getSetting('leveling.enabled')) {
await handleLeveling(message);
Expand Down
15 changes: 13 additions & 2 deletions src/events/ready.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { scheduleJob } from 'node-schedule';
import { PresenceUpdateStatus, ActivityType, Routes, REST } from 'discord.js';
import { setupGuild } from '@/setup-guild';
import getUserInfo from '@/commands/user-info';
import banCommand from '@/commands/ban';
import kickCommand from '@/commands/kick';
import settingsCommand from '@/commands/settings';
Expand All @@ -15,19 +17,27 @@ import banContextMenu from '@/context-menus/users/ban';
import { checkMatchmakingThreads } from '@/matchmaking-threads';
import { SlowMode } from '@/models/slow-mode';
import handleSlowMode from '@/slow-mode';
import config from '@/config';
import type { Client } from 'discord.js';

export async function readyHandler(client: Client): Promise<void> {
console.log('Registering global commands');
const rest = new REST({ version: '10' }).setToken(config.bot_token);

loadBotHandlersCollection(client);

console.log('Setting up guilds');
const guilds = await client.guilds.fetch();
for (const id of guilds.keys()) {
const guild = await guilds.get(id)!.fetch();
await setupGuild(guild);
await setupGuild(guild, rest);
}

console.log('Registering global commands');
// for global commands
await rest.put(Routes.applicationCommands(client.user!.id), { body: [getUserInfo.deploy] })
.then(() => console.log('Successfully registered user-info command.'))
.catch(console.error);

scheduleJob('*/10 * * * *', async () => {
await checkMatchmakingThreads();
});
Expand All @@ -48,6 +58,7 @@ function loadBotHandlersCollection(client: Client): void {
client.commands.set(slowModeCommand.name, slowModeCommand);
client.commands.set(removeWarnCommand.name, removeWarnCommand);
client.commands.set(listWarnsCommand.name, listWarnsCommand);
client.commands.set(getUserInfo.name, getUserInfo);

client.contextMenus.set(messageLogContextMenu.name, messageLogContextMenu);
client.contextMenus.set(warnContextMenu.name, warnContextMenu);
Expand Down
16 changes: 7 additions & 9 deletions src/setup-guild.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v10';
import config from '@/config';
import type { Guild } from 'discord.js';
import type { Guild, REST } from 'discord.js';

const rest = new REST({ version: '10' }).setToken(config.bot_token);

export async function setupGuild(guild: Guild): Promise<void> {
export async function setupGuild(guild: Guild, rest: REST): Promise<void> {
// * Populate members cache
await guild.members.fetch();

try {
// * Setup commands
await deployCommands(guild);
await deployCommands(guild, rest);
} catch (error) {
console.error(`Failed to deploy commands for guild ${guild.id}:`, error);
}
}

async function deployCommands(guild: Guild): Promise<void> {
const commands = guild.client.commands.map((command) => {
async function deployCommands(guild: Guild, rest: REST): Promise<void> {
const commands = guild.client.commands.filter((c) => {
return c.name !== 'user-info';
}).map((command) => {
return command.deploy;
});

Expand Down