diff --git a/src/commands/ban.ts b/src/commands/ban.ts index 8e498d2..756e577 100644 --- a/src/commands/ban.ts +++ b/src/commands/ban.ts @@ -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'; @@ -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') diff --git a/src/commands/kick.ts b/src/commands/kick.ts index a27c94d..c23d958 100644 --- a/src/commands/kick.ts +++ b/src/commands/kick.ts @@ -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'; @@ -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') diff --git a/src/commands/list-warns.ts b/src/commands/list-warns.ts index ce33eb1..c4bdd8f 100644 --- a/src/commands/list-warns.ts +++ b/src/commands/list-warns.ts @@ -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'; @@ -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 by <@${warn.content.admin_user_id}>` }); }); @@ -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') diff --git a/src/commands/modping.ts b/src/commands/modping.ts index 6ce2540..8977867 100644 --- a/src/commands/modping.ts +++ b/src/commands/modping.ts @@ -1,3 +1,4 @@ +import { InteractionContextType } from 'discord.js'; import { SlashCommandBuilder } from '@discordjs/builders'; import { ModPingSettings } from '@/models/modPingSettings'; import { getRoleFromSettings } from '@/util'; @@ -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)') diff --git a/src/commands/remove-warn.ts b/src/commands/remove-warn.ts index 4a72d46..f77dbd9 100644 --- a/src/commands/remove-warn.ts +++ b/src/commands/remove-warn.ts @@ -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'; @@ -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') diff --git a/src/commands/settings.ts b/src/commands/settings.ts index 0dea6e7..1700257 100644 --- a/src/commands/settings.ts +++ b/src/commands/settings.ts @@ -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'; @@ -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'); diff --git a/src/commands/slow-mode.ts b/src/commands/slow-mode.ts index a1f48c5..b4e996d 100644 --- a/src/commands/slow-mode.ts +++ b/src/commands/slow-mode.ts @@ -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'; @@ -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') diff --git a/src/commands/user-info.ts b/src/commands/user-info.ts new file mode 100644 index 0000000..63c5db2 --- /dev/null +++ b/src/commands/user-info.ts @@ -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 { + 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: ` (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() +}; diff --git a/src/commands/warn.ts b/src/commands/warn.ts index 8d4c97d..a687562 100644 --- a/src/commands/warn.ts +++ b/src/commands/warn.ts @@ -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'; @@ -183,6 +183,8 @@ 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`, @@ -190,7 +192,7 @@ export async function warnHandler(interaction: CommandInteraction | ModalSubmitI }, { name: 'Date', - value: warning.timestamp.toLocaleDateString(), + value: ` `, inline: true } ); @@ -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') diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index fa8b716..76e505e 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -10,7 +10,7 @@ export default async function messageCreateHandler(message: Message): Promise)**.'); + 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]()**.\n\nTo view your warns, run Chubby\'s `/user-info` command.'); } else { if (await getSetting('leveling.enabled')) { await handleLeveling(message); diff --git a/src/events/ready.ts b/src/events/ready.ts index 442e3e8..d625301 100644 --- a/src/events/ready.ts +++ b/src/events/ready.ts @@ -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'; @@ -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 { - 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(); }); @@ -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); diff --git a/src/setup-guild.ts b/src/setup-guild.ts index 05656f0..ca8f067 100644 --- a/src/setup-guild.ts +++ b/src/setup-guild.ts @@ -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 { +export async function setupGuild(guild: Guild, rest: REST): Promise { // * 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 { - const commands = guild.client.commands.map((command) => { +async function deployCommands(guild: Guild, rest: REST): Promise { + const commands = guild.client.commands.filter((c) => { + return c.name !== 'user-info'; + }).map((command) => { return command.deploy; });