Merge branch 'warnings'
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
import { ButtonInteraction, Client } from 'discord.js';
|
||||
import { getWarningMessage } from '../utils/warning-utils';
|
||||
|
||||
export default {
|
||||
name: 'warning-next',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getWarningMessage(userId, parseInt(page) + 1);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { ButtonInteraction, Client} from 'discord.js';
|
||||
import { getWarningMessage } from '../utils/warning-utils';
|
||||
|
||||
export default {
|
||||
name: 'warning-previous',
|
||||
handler: async function (client: Client, interaction: ButtonInteraction, userId: string, page: string) {
|
||||
await interaction.deferUpdate();
|
||||
|
||||
const message = await getWarningMessage(userId, parseInt(page) - 1);
|
||||
|
||||
if (!message) return;
|
||||
|
||||
interaction.editReply(message);
|
||||
}
|
||||
};
|
||||
@@ -147,7 +147,7 @@ async function dumpPhrases(interaction: ChatInputCommandInteraction) {
|
||||
}
|
||||
|
||||
async function addPhrase(interaction: ChatInputCommandInteraction, phrase: string, group: SubcommandGroup) {
|
||||
await Database.addTicketPhrase(group == 'admin' ? 'admin' : interaction.user.id, phrase);
|
||||
await Database.putTicketPhrase(group == 'admin' ? 'admin' : interaction.user.id, phrase);
|
||||
interaction.reply(`Phrases matching "${phrase}" will now alert ${group == 'admin' ? 'admins' : 'you'}.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,12 +65,14 @@ export default {
|
||||
.setRequired(false)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
|
||||
let response = '';
|
||||
|
||||
const settings = await Database.getGuildSettings(interaction.guildId!);
|
||||
|
||||
if (!settings) {
|
||||
await Database.addGuild(interaction.guildId!);
|
||||
await Database.putGuild(interaction.guildId!);
|
||||
}
|
||||
|
||||
const generalChannel = interaction.options.getChannel('general-channel');
|
||||
@@ -132,7 +134,7 @@ export default {
|
||||
const addCategory = interaction.options.getChannel('add-staff-category');
|
||||
if (addCategory) {
|
||||
if (addCategory.type == ChannelType.GuildCategory) {
|
||||
await Database.addGuildStaffCategory(interaction.guildId!, addCategory.id);
|
||||
await Database.putGuildStaffCategory(interaction.guildId!, addCategory.id);
|
||||
|
||||
response += `Added ${addCategory.toString()} as a staff category.\n`;
|
||||
} else {
|
||||
@@ -153,8 +155,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (response.length == 0) return interaction.reply({ content: 'No settings provided.', flags: [MessageFlags.Ephemeral] });
|
||||
if (response.length == 0) return interaction.editReply({ content: 'No settings provided.' });
|
||||
|
||||
interaction.reply({ content: response, flags: [MessageFlags.Ephemeral] });
|
||||
interaction.editReply({ content: response });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import { ApplicationCommandOptionType, ApplicationIntegrationType, AutocompleteInteraction, ChatInputCommandInteraction, Client, GuildBasedChannel, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { channelIsInStaffCategory, getE621User } from '../utils';
|
||||
import { config } from '../config';
|
||||
import { getWarningMessage } from '../utils/warning-utils';
|
||||
|
||||
export default {
|
||||
name: 'warnings',
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('warnings')
|
||||
.setDescription('Add, view, or remove user warnings.')
|
||||
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
|
||||
.setContexts(InteractionContextType.Guild)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('add')
|
||||
.setDescription('Add warnings to a user.')
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The user to add a warning to.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reason')
|
||||
.setDescription('The reason for the warning.')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove warnings from a user.')
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The user to remove a warning from.')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option
|
||||
.setName('warning')
|
||||
.setDescription('The warning to remove.')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription("List a user's warnings")
|
||||
.addUserOption(option =>
|
||||
option
|
||||
.setName('user')
|
||||
.setDescription('The user to list the warnings of.')
|
||||
.setRequired(true)
|
||||
)
|
||||
),
|
||||
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
|
||||
const subcommand = interaction.options.getSubcommand(true);
|
||||
const user = interaction.options.getUser('user', true);
|
||||
|
||||
const isStaffChannel = await channelIsInStaffCategory(interaction.channel as GuildBasedChannel);
|
||||
|
||||
if (isStaffChannel) await interaction.deferReply();
|
||||
else await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
|
||||
|
||||
if (subcommand == 'add') {
|
||||
const reason = interaction.options.getString('reason', true);
|
||||
|
||||
await Database.putWarning(user.id, reason, interaction.user.id);
|
||||
|
||||
if (isStaffChannel) interaction.editReply('Added warning.');
|
||||
else interaction.editReply({ content: 'Added warning.' });
|
||||
} else if (subcommand == 'remove') {
|
||||
const warningId = interaction.options.getInteger('warning', true);
|
||||
|
||||
if (await Database.removeWarning(warningId)) {
|
||||
if (isStaffChannel) interaction.editReply('Removed warning.');
|
||||
else interaction.editReply({ content: 'Removed warning.' });
|
||||
} else {
|
||||
if (isStaffChannel) interaction.editReply('Warning not found.');
|
||||
else interaction.editReply({ content: 'Warning not found.' });
|
||||
}
|
||||
} else if (subcommand == 'list') {
|
||||
const warningMessage = await getWarningMessage(user.id, 1);
|
||||
|
||||
if (!warningMessage) return interaction.editReply(`No warnings found for <@${user.id}>`);
|
||||
|
||||
interaction.editReply(warningMessage);
|
||||
}
|
||||
},
|
||||
autoComplete: async function (client: Client, interaction: AutocompleteInteraction) {
|
||||
const user = interaction.options.get('user');
|
||||
|
||||
if (!user || user.type != ApplicationCommandOptionType.User) return interaction.respond([]);
|
||||
|
||||
const userId = user.value as string;
|
||||
|
||||
const value = interaction.options.getFocused().toLowerCase();
|
||||
|
||||
const warnings = await Database.getWarnings(userId);
|
||||
|
||||
const toRespond = warnings.filter(w => !value ? true : w.reason.toLowerCase().includes(value));
|
||||
if (toRespond.length > 25) toRespond.length = 25;
|
||||
|
||||
interaction.respond(toRespond.map((w) => {
|
||||
return {
|
||||
name: w.reason.substring(0, 50),
|
||||
value: w.id
|
||||
};
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { getE621Post, getE621PostByMd5, getPostUrl, hasBlacklistedTags } from '.
|
||||
import { Database } from '../shared/Database';
|
||||
import { logDeletion, logEdit } from '../utils/message-logger';
|
||||
import { isEdited } from '../utils/message-utils';
|
||||
import { channelIsInStaffCategory } from '../utils';
|
||||
|
||||
export type Message<InGuild extends boolean = boolean> = OmitPartialGroupDMChannel<DiscordMessage<InGuild>>;
|
||||
export type Partial = OmitPartialGroupDMChannel<PartialMessage>;
|
||||
@@ -142,12 +143,11 @@ async function wikiPageHandler(message: Message, matchedGroups: RegExpExecArray[
|
||||
}
|
||||
|
||||
async function blacklistIfNecessary(message: Message, posts: E621Post[]): Promise<boolean> {
|
||||
const staffCategories = await Database.getGuildStaffCategories(message.guildId!);
|
||||
|
||||
const blacklistedIds: number[] = [];
|
||||
|
||||
const channel = await message.channel.fetch() as GuildTextBasedChannel;
|
||||
|
||||
const isStaffChannel = await channelIsInStaffCategory(channel);
|
||||
|
||||
for (const post of posts) {
|
||||
if (hasBlacklistedTags(post)) {
|
||||
@@ -159,7 +159,7 @@ async function blacklistIfNecessary(message: Message, posts: E621Post[]): Promis
|
||||
|
||||
await message.delete();
|
||||
|
||||
if (channel.parentId && staffCategories.includes(channel.parentId)) {
|
||||
if (channel.parentId && isStaffChannel) {
|
||||
await message.channel.send({
|
||||
content: `_sucks message into the void._ ${message.author.toString()} nono, don't post links to ${blacklistedIds.length == 1 ? `post ${blacklistedIds[0]}` : `posts \`${blacklistedIds.join('`, `')}\``}. See rule #5.b for more details.`,
|
||||
allowedMentions: {
|
||||
|
||||
+6
-5
@@ -1,5 +1,5 @@
|
||||
import 'source-map-support/register';
|
||||
import { Client as DiscordClient, GatewayIntentBits, Guild, Partials } from 'discord.js';
|
||||
import { Client as DiscordClient, GatewayIntentBits, Guild, MessageFlags, Partials } from 'discord.js';
|
||||
import { config } from './config';
|
||||
import { Handler } from './types';
|
||||
import { initIfNecessary, loadHandlersFrom, openRedisClient, refreshCommands } from './utils';
|
||||
@@ -52,7 +52,7 @@ client.on('interactionCreate', async (interaction) => {
|
||||
)
|
||||
interaction.reply({
|
||||
content: 'Bot is still starting up. Please wait a few seconds.',
|
||||
ephemeral: true,
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -84,7 +84,7 @@ client.on('interactionCreate', async (interaction) => {
|
||||
|
||||
for (const button of buttons) {
|
||||
if (id == button.name) {
|
||||
button.handler(client, interaction, interaction.customId.split('_')[1]);
|
||||
button.handler(client, interaction, ...interaction.customId.split('_').slice(1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ client.on('interactionCreate', async (interaction) => {
|
||||
|
||||
for (const modal of modals) {
|
||||
if (id == modal.name) {
|
||||
modal.handler(client, interaction, interaction.customId.split('_')[1]);
|
||||
modal.handler(client, interaction, ...interaction.customId.split('_').slice(1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ client.on('interactionCreate', async (interaction) => {
|
||||
|
||||
for (const menu of menus) {
|
||||
if (id == menu.name) {
|
||||
menu.handler(client, interaction, interaction.customId.split('_')[1]);
|
||||
menu.handler(client, interaction, ...interaction.customId.split('_').slice(1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -151,5 +151,6 @@ client.on('threadCreate', handleThreadCreate);
|
||||
client.on('voiceStateUpdate', handleVoiceStateUpdate);
|
||||
|
||||
client.on('error', console.error);
|
||||
process.on('uncaughtException', console.error);
|
||||
|
||||
client.login(config.DISCORD_TOKEN);
|
||||
+48
-6
@@ -3,7 +3,7 @@ import { open, Database as SqliteDatabase } from 'sqlite';
|
||||
import { config } from '../config';
|
||||
import DiscordOAuth2 from 'discord-oauth2';
|
||||
import { serializeMessage, wait } from '../utils';
|
||||
import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase } from '../types';
|
||||
import { GuildSettings, LoggedMessage, TicketMessage, TicketPhrase, Warning } from '../types';
|
||||
import { Message } from '../events';
|
||||
|
||||
const DB_SCHEMA = `
|
||||
@@ -52,6 +52,14 @@ const DB_SCHEMA = `
|
||||
user_id TEXT NOT NULL,
|
||||
phrase TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS warnings (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
reason TEXT,
|
||||
mod_id TEXT,
|
||||
timestamp datetime NOT NULL DEFAULT (datetime('now', 'localtime'))
|
||||
);
|
||||
`;
|
||||
|
||||
export class Database {
|
||||
@@ -75,6 +83,8 @@ export class Database {
|
||||
console.log('SQLite database ensured');
|
||||
}
|
||||
|
||||
// -- START WHOIS --
|
||||
|
||||
static async getE621Ids(discordId: string): Promise<number[]> {
|
||||
const ids = await Database.db.all<{ user_id: number }[]>('SELECT DISTINCT user_id FROM discord_names WHERE discord_id = ?', discordId);
|
||||
|
||||
@@ -87,7 +97,7 @@ export class Database {
|
||||
return ids.map(r => r.discord_id);
|
||||
}
|
||||
|
||||
static async getCombinedIds(id: string) {
|
||||
static async getCombinedIds(id: string): Promise<{ userId: string, discordId: string }[]> {
|
||||
const ids = await Database.db.all<{ discord_id: string, user_id: number }[]>(`
|
||||
WITH RECURSIVE rec AS (
|
||||
SELECT DISTINCT d1.user_id, d1.discord_id, 1 AS depth FROM discord_names d1 WHERE d1.user_id = ? or d1.discord_id = ?
|
||||
@@ -98,18 +108,22 @@ export class Database {
|
||||
WHERE depth <= 5 AND rec.depth = depth
|
||||
) SELECT DISTINCT user_id, discord_id FROM rec`, id, id);
|
||||
|
||||
return ids.map(r => ({ user_id: r.user_id.toString(), discord_id: r.discord_id }));
|
||||
return ids.map(r => ({ userId: r.user_id.toString(), discordId: r.discord_id }));
|
||||
}
|
||||
|
||||
static async putUser(id: number, user: DiscordOAuth2.User) {
|
||||
await Database.db.run('INSERT INTO discord_names(user_id, discord_id, discord_username) VALUES (?, ?, ?)', id, user.id, user.username);
|
||||
}
|
||||
|
||||
// -- END WHOIS --
|
||||
|
||||
// -- START SETTINGS --
|
||||
|
||||
static async getGuildSettings(guildId: string): Promise<GuildSettings | undefined> {
|
||||
return await Database.db.get<GuildSettings>('SELECT * FROM settings WHERE guild_id = ?', guildId);
|
||||
}
|
||||
|
||||
static async addGuild(guildId: string) {
|
||||
static async putGuild(guildId: string) {
|
||||
await Database.db.run('INSERT INTO settings(guild_id) VALUES (?)', guildId);
|
||||
}
|
||||
|
||||
@@ -149,7 +163,7 @@ export class Database {
|
||||
return settings.staff_categories.split(',');
|
||||
}
|
||||
|
||||
static async addGuildStaffCategory(guildId: string, categoryId: string) {
|
||||
static async putGuildStaffCategory(guildId: string, categoryId: string) {
|
||||
const categories = await Database.getGuildStaffCategories(guildId);
|
||||
|
||||
categories.push(categoryId);
|
||||
@@ -174,6 +188,10 @@ export class Database {
|
||||
return true;
|
||||
}
|
||||
|
||||
// -- END SETTINGS --
|
||||
|
||||
// START MESSAGE LOGS --
|
||||
|
||||
static async putMessage(message: Message): Promise<boolean> {
|
||||
try {
|
||||
const serializedMessage = serializeMessage(message);
|
||||
@@ -206,6 +224,10 @@ export class Database {
|
||||
}
|
||||
}
|
||||
|
||||
// -- END MESSAGE LOGS --
|
||||
|
||||
// -- START TICKETS --
|
||||
|
||||
static async putTicket(ticketId: number, messageId: string) {
|
||||
await Database.db.run('INSERT INTO tickets(id, message_id) VALUES (?, ?)', ticketId, messageId);
|
||||
}
|
||||
@@ -215,7 +237,7 @@ export class Database {
|
||||
return ticket?.message_id;
|
||||
}
|
||||
|
||||
static async addTicketPhrase(userId: string, phrase: string) {
|
||||
static async putTicketPhrase(userId: string, phrase: string) {
|
||||
await Database.db.run('INSERT INTO ticket_phrases(user_id, phrase) VALUES (?, ?)', userId, phrase);
|
||||
}
|
||||
|
||||
@@ -234,4 +256,24 @@ export class Database {
|
||||
cb(ticketPhrase);
|
||||
});
|
||||
}
|
||||
|
||||
// -- END TICKETS --
|
||||
|
||||
// -- START WARNINGS --
|
||||
|
||||
static async putWarning(userId: string, reason: string, modId: string) {
|
||||
await Database.db.run('INSERT INTO warnings(user_id, reason, mod_id) VALUES (?, ?, ?)', userId, reason, modId);
|
||||
}
|
||||
|
||||
static async removeWarning(id: number): Promise<boolean> {
|
||||
const res = await Database.db.run('DELETE from warnings WHERE id = ?', id);
|
||||
|
||||
return (res.changes ?? 0) > 0;
|
||||
}
|
||||
|
||||
static async getWarnings(userId: string): Promise<Warning[]> {
|
||||
return await Database.db.all<Warning[]>('SELECT * from warnings WHERE user_id = ?', userId);
|
||||
}
|
||||
|
||||
// -- END WARNINGS --
|
||||
}
|
||||
@@ -31,3 +31,11 @@ export type TicketPhrase = {
|
||||
user_id: string
|
||||
phrase: string
|
||||
}
|
||||
|
||||
export type Warning = {
|
||||
id: number
|
||||
user_id: string
|
||||
reason: string
|
||||
mod_id: string
|
||||
timestamp: string
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
export * from './command';
|
||||
export * from './database-types';
|
||||
export * from './e621-types';
|
||||
export * from './handler';
|
||||
export * from './helper-types';
|
||||
export * from './database-types';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Channel, GuildBasedChannel, GuildChannel, GuildTextBasedChannel, TextBasedChannel, VoiceBasedChannel } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
|
||||
export async function channelIsInStaffCategory(channel: GuildBasedChannel) {
|
||||
const staffCategories = await Database.getGuildStaffCategories(channel.guildId);
|
||||
|
||||
return staffCategories.includes(channel.parentId!);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './array-utils';
|
||||
export * from './audit-log-utils';
|
||||
export * from './channel-utils';
|
||||
export * from './commands';
|
||||
export * from './e621-ticket-listener';
|
||||
export * from './e621-utils';
|
||||
@@ -11,3 +12,4 @@ export * from './string-utils';
|
||||
export * from './ticket-utils';
|
||||
export * from './wait';
|
||||
export * from './whois';
|
||||
export * from './warning-utils';
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ActionRowBuilder, APIEmbedField, ButtonBuilder, ButtonStyle, EmbedBuilder, GuildMember, MessageActionRowComponentBuilder, time } from 'discord.js';
|
||||
import { Database } from '../shared/Database';
|
||||
import { Warning } from '../types';
|
||||
|
||||
type MessageContent = { content: string, components: ActionRowBuilder<ButtonBuilder>[] };
|
||||
|
||||
const WARNINGS_PER_PAGE = 5;
|
||||
|
||||
function getWarningText(warning: Warning): string {
|
||||
const timestamp = time(new Date(warning.timestamp));
|
||||
|
||||
return `### Warning from <@${warning.mod_id}> (${timestamp}):\n${warning.reason}`;
|
||||
}
|
||||
|
||||
export async function getWarningMessage(userId: string, page: number): Promise<MessageContent | null> {
|
||||
const warnings = await Database.getWarnings(userId);
|
||||
|
||||
page = page - 1;
|
||||
|
||||
const maxPage = Math.floor(warnings.length / WARNINGS_PER_PAGE);
|
||||
|
||||
if (warnings.length == 0) return null;
|
||||
|
||||
const warningTexts: string[] = [];
|
||||
|
||||
for (let i = page * WARNINGS_PER_PAGE; i < page * WARNINGS_PER_PAGE + WARNINGS_PER_PAGE; i++) {
|
||||
if (i >= warnings.length) break;
|
||||
|
||||
warningTexts.push(getWarningText(warnings[i]));
|
||||
}
|
||||
|
||||
const prevPage = new ButtonBuilder()
|
||||
.setLabel('Previous Page')
|
||||
.setCustomId(`warning-previous_${userId}_${page + 1}`)
|
||||
.setDisabled(page == 0)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const nextPage = new ButtonBuilder()
|
||||
.setLabel('Next Page')
|
||||
.setCustomId(`warning-next_${userId}_${page + 1}`)
|
||||
.setDisabled(page >= maxPage)
|
||||
.setStyle(ButtonStyle.Primary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(prevPage, nextPage);
|
||||
|
||||
return {
|
||||
content: `<@${userId}>'s Warnings\n` + warningTexts.join('\n\n') + `\n\n-# Page ${page + 1}/${maxPage + 1}`,
|
||||
components: [row]
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user