13 changed files with 112 additions and 123 deletions
+8 -4
View File
@@ -1,5 +1,9 @@
node_modules # Directories
dist certs/
data/* data/
dist/
node_modules/
# Files
.DS_Store
.env .env
certs/*
@@ -0,0 +1,9 @@
--------------------------------------------------------------------------------
-- Up
--------------------------------------------------------------------------------
ALTER TABLE settings DROP COLUMN devwatch_role_id;
--------------------------------------------------------------------------------
-- Down
--------------------------------------------------------------------------------
ALTER TABLE settings ADD devwatch_role_id TEXT;
+8 -25
View File
@@ -1,37 +1,20 @@
import { ButtonInteraction, Client, MessageFlags } from 'discord.js'; import { ButtonInteraction, Client, MessageFlags } from 'discord.js';
import { Database } from '../shared/Database';
import { Button } from '../types'; import { Button } from '../types';
import { SetSeverity } from '../utils';
class DevWatchButton implements Button<Client> { class DevWatchButton implements Button<Client> {
id = 'dev-watch'; id = 'dev-watch';
async execute(context: Client<boolean>, interaction: ButtonInteraction): Promise<void> { async execute(context: Client<boolean>, interaction: ButtonInteraction): Promise<void> {
if (!interaction.inGuild()) return; if (!interaction.inGuild()) return;
await interaction.deferReply({ flags: MessageFlags.Ephemeral }); await interaction.reply({
embeds: [{
...SetSeverity('warning'),
description: 'DevWatch roles have been deprecated in favour of the `/role-buttons create` command. Please notify an administrator to setup a new role-button.'
}],
try { flags: MessageFlags.Ephemeral,
const settings = await Database.getOrCreateSettings(interaction.guildId); });
if (!settings.devwatch_role_id) throw new Error('`devwatch_role_id` was not configured. Please configure a role then try again.');
const guild = await context.guilds.fetch(interaction.guildId);
const member = await guild.members.fetch(interaction.user.id);
if (!member) throw new Error('Unable to fetch the member executing this command.');
const bot = await guild.members.fetchMe();
const role = await guild.roles.fetch(settings.devwatch_role_id);
if (!role) throw new Error('The configured `devwatch_role_id` does not exist.');
if (role.position > bot.roles.highest.position) throw new Error('The configured `devwatch_role_id` is higher than my highest role.');
if (member.roles.cache.has(settings.devwatch_role_id)) {
await member.roles.remove(settings.devwatch_role_id);
await interaction.editReply({ content: 'Removed role.' });
} else {
await member.roles.add(settings.devwatch_role_id);
await interaction.editReply({ content: 'Added role.' });
}
} catch (error: Error | any) {
await interaction.editReply({ content: error.message }).catch(() => {});
}
} }
} }
-42
View File
@@ -1,42 +0,0 @@
import { ActionRowBuilder, ApplicationIntegrationType, ButtonBuilder, ButtonStyle, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
export default {
name: 'devwatch',
data: new SlashCommandBuilder()
.setName('devwatch')
.setDescription('Sends a dev watch role toggle button.')
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
.setContexts(InteractionContextType.Guild)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
.addStringOption(option =>
option
.setName('content')
.setDescription('The content of the message.')
.setRequired(false)
)
.addStringOption(option =>
option
.setName('button-label')
.setDescription('The button label.')
.setRequired(false)
),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
if (!interaction.channel || !interaction.channel.isSendable())
return interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Missing permissions to send to channel.' });
const content = interaction.options.getString('content') ?? '';
const label = interaction.options.getString('button-label') ?? 'Toggle DevWatch Role';
const button = new ButtonBuilder()
.setCustomId('dev-watch')
.setStyle(ButtonStyle.Primary)
.setLabel(label);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(button);
await interaction.channel.send({ components: [row], content });
interaction.reply({ flags: [MessageFlags.Ephemeral], content: 'Sent.' });
}
};
+66 -31
View File
@@ -1,6 +1,61 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { Database } from '../shared/Database'; import { Database } from '../shared/Database';
import { getIdFromInput } from '../utils'; import { getIdFromInput, SetSeverity } from '../utils';
const userRegex = new RegExp(/^\d{16,20}$/);
async function addUserMapping(interaction: ChatInputCommandInteraction) {
const githubUsername = interaction.options.getString('github-name', true);
const discordId = getIdFromInput(interaction.options.getString('discord-user', true));
if (!userRegex.test(discordId)) throw new Error('The provided Discord user ID is invalid.');
const existingMappingId = await Database.getGithubFromDiscordId(discordId);
const existingMappingName = await Database.getDiscordIdFromGithub(githubUsername);
if (existingMappingId) throw new Error(`<@${discordId}> (${discordId}) is already mapped to \`${existingMappingId}\``);
if (existingMappingName) throw new Error(`\`${githubUsername}\` is already mapped to <@${existingMappingName}> (${existingMappingName})`);
await Database.putGithubUserMapping(discordId, githubUsername);
await interaction.editReply({
embeds: [{
...SetSeverity('success'),
title: 'GitHub/Discord Mapping | Add',
description: `\`${githubUsername}\` has been linked to <@${discordId}>`,
}],
});
}
async function removeUserMapping(interaction: ChatInputCommandInteraction) {
const discordId = getIdFromInput(interaction.options.getString('discord-user', true));
if (!userRegex.test(discordId)) throw new Error('The provided Discord user ID is invalid.');
if (!await Database.removeGithubUserMapping(discordId)) throw new Error('No existing GitHub mapping was found.');
await interaction.editReply({
embeds: [{
...SetSeverity('success'),
title: 'GitHub/Discord Mapping | Remove',
description: `<@${discordId}> is no longer linked.`,
}],
});
}
async function listUserMapping(interaction: ChatInputCommandInteraction) {
const allMappings = await Database.getAllGithubUserMappings();
if (allMappings.length < 1) throw new Error('No GitHub mappings have been configured.');
await interaction.editReply({
embeds: [{
...SetSeverity('success'),
title: 'GitHub/Discord Mapping | List',
description: allMappings.map(m => `- <@${m.discord_id}> (${m.discord_id}) - ${m.github_username}`).join('\n'),
}]
});
}
export default { export default {
name: 'github-mapping', name: 'github-mapping',
@@ -44,37 +99,17 @@ export default {
.setDescription('List all github-discord mappings.') .setDescription('List all github-discord mappings.')
), ),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) { handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] }); if (!interaction.inGuild()) return;
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const subcommand = await interaction.options.getSubcommand(true); try {
switch (interaction.options.getSubcommand(true)) {
if (subcommand == 'add') { case 'add': return await addUserMapping(interaction);
const discordUserInput = interaction.options.getString('discord-user', true); case 'remove': return await removeUserMapping(interaction);
case 'list': return await listUserMapping(interaction);
const idToUse = getIdFromInput(discordUserInput); }
} catch (error: Error | any) {
const githubName = interaction.options.getString('github-name', true); await interaction.editReply({ content: error.message }).catch(() => {});
const existingMappingId = await Database.getGithubFromDiscordId(idToUse);
const existingMappingName = await Database.getDiscordIdFromGithub(githubName);
if (existingMappingId) return interaction.editReply(`Discord user id is already mapped to ${existingMappingId}`);
if (existingMappingName) return interaction.editReply(`Github username is already mapped to <@${existingMappingName}> (${existingMappingName})`);
Database.putGithubUserMapping(idToUse, githubName);
return interaction.editReply('Mapping added.');
} else if (subcommand == 'remove') {
const discordUserInput = interaction.options.getString('discord-user', true);
const idToUse = getIdFromInput(discordUserInput);
Database.removeGithubUserMapping(idToUse);
return interaction.editReply('Mapping removed.');
} else if (subcommand == 'list') {
const allMappings = await Database.getAllGithubUserMappings();
return interaction.editReply(allMappings.map(m => `- <@${m.discord_id}> (${m.discord_id}) - ${m.github_username}`).join('\n'));
} }
} }
}; };
+1 -12
View File
@@ -75,12 +75,7 @@ export default {
.setDescription('Set the private helper role.') .setDescription('Set the private helper role.')
.setRequired(false) .setRequired(false)
) )
.addRoleOption(option =>
option
.setName('devwatch-role')
.setDescription('Set the DevWatch role.')
.setRequired(false)
)
.addChannelOption(option => .addChannelOption(option =>
option option
.setName('add-staff-category') .setName('add-staff-category')
@@ -204,12 +199,6 @@ export default {
response.push(`**private_help_role_id** has been set to: ${privateHelperRole}.`); response.push(`**private_help_role_id** has been set to: ${privateHelperRole}.`);
} }
const devWatchRole = interaction.options.getRole('devwatch-role');
if (devWatchRole) {
await Database.updateSettings(interaction.guildId, 'devwatch_role_id', devWatchRole.id);
response.push(`**devwatch_role_id** has been set to ${devWatchRole}.`);
}
const addCategory = interaction.options.getChannel('add-staff-category'); const addCategory = interaction.options.getChannel('add-staff-category');
if (addCategory) { if (addCategory) {
if (addCategory.type == ChannelType.GuildCategory) { if (addCategory.type == ChannelType.GuildCategory) {
+6
View File
@@ -1,4 +1,5 @@
import { Client, Guild } from 'discord.js'; import { Client, Guild } from 'discord.js';
import { config } from '../../config';
import { Database } from '../../shared/Database'; import { Database } from '../../shared/Database';
import { Event } from '../../types'; import { Event } from '../../types';
@@ -6,6 +7,11 @@ class GuildCreateEvent implements Event<Client, 'guildCreate'> {
event = 'guildCreate' as const; event = 'guildCreate' as const;
async execute(context: Client<boolean>, guild: Guild): Promise<void> { async execute(context: Client<boolean>, guild: Guild): Promise<void> {
if (guild.id != config.DISCORD_GUILD_ID) {
await guild.leave();
return;
}
await Database.getOrCreateSettings(guild.id); await Database.getOrCreateSettings(guild.id);
} }
} }
+4
View File
@@ -126,6 +126,10 @@ client.on('interactionCreate', async (interaction) => {
client.on('clientReady', async () => { client.on('clientReady', async () => {
console.log(`Logged in as ${client.user!.tag}!`); console.log(`Logged in as ${client.user!.tag}!`);
for (const guild of client.guilds.cache.values()) {
if (guild.id != config.DISCORD_GUILD_ID) await guild.leave();
}
await loadHandlersFrom('commands', commands); await loadHandlersFrom('commands', commands);
await loadHandlersFrom('context-menus', contextMenus); await loadHandlersFrom('context-menus', contextMenus);
await loadHandlersFrom('menus', menus); await loadHandlersFrom('menus', menus);
+3 -2
View File
@@ -364,8 +364,9 @@ export class Database {
}); });
} }
static async removeGithubUserMapping(discordId: string) { static async removeGithubUserMapping(discordId: string): Promise<boolean> {
await Database.db.run('DELETE from github_user_mapping WHERE discord_id_hash = ?', Encrypter.encrypt(discordId)); const result = await Database.db.run('DELETE FROM github_user_mapping WHERE discord_id_hash = ?', Encrypter.hash(discordId));
return (result.changes ?? 0) > 0;
} }
//#endregion //#endregion
-1
View File
@@ -22,7 +22,6 @@ export type GuildSettings = {
voice_logs_channel_id?: string voice_logs_channel_id?: string
admin_role_id?: string admin_role_id?: string
private_help_role_id?: string private_help_role_id?: string
devwatch_role_id?: string
staff_categories?: string staff_categories?: string
safe_channels?: string safe_channels?: string
link_skip_channels?: string link_skip_channels?: string
+2 -1
View File
@@ -21,7 +21,8 @@ export function createTextInput(customId: string, labelTitle: string, descriptio
export function createCheckbox(customId: string, labelTitle: string, description: string | null, defaultYes: boolean): LabelBuilder { export function createCheckbox(customId: string, labelTitle: string, description: string | null, defaultYes: boolean): LabelBuilder {
const component = new CheckboxBuilder() const component = new CheckboxBuilder()
.setCustomId(customId); .setCustomId(customId)
.setDefault(defaultYes);
const yesNoLabel = new LabelBuilder() const yesNoLabel = new LabelBuilder()
.setLabel(labelTitle) .setLabel(labelTitle)