From 319f586565a69bcfa19e929721ba42bf74cfaa7f Mon Sep 17 00:00:00 2001 From: "Jill \"oatmealine\" Monoids" Date: Mon, 27 Nov 2023 18:14:08 +0300 Subject: [PATCH] unhardcode self-assignable role stuff this is pretty old stuff and i'm glad i revamped it bc it's a very good system --- migrations/20231127142531_roleSeperators.js | 21 ++++++ src/commands/color.ts | 13 ++-- src/commands/garbagecollectroles.ts | 12 ++-- src/commands/pronouns.ts | 13 ++-- src/commands/roles.ts | 79 +++++++++++++++++++++ src/lib/assignableRoles.ts | 15 +++- src/lib/db.ts | 6 ++ 7 files changed, 138 insertions(+), 21 deletions(-) create mode 100644 migrations/20231127142531_roleSeperators.js create mode 100644 src/commands/roles.ts diff --git a/migrations/20231127142531_roleSeperators.js b/migrations/20231127142531_roleSeperators.js new file mode 100644 index 0000000..5de7251 --- /dev/null +++ b/migrations/20231127142531_roleSeperators.js @@ -0,0 +1,21 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function(knex) { + return knex.schema + .createTable('roleSeperators', table => { + table.string('guild'); + table.string('role'); + table.enum('type', ['color', 'pronoun']); + }); +}; + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function(knex) { + return knex.schema + .dropTable('roleSeperators'); +}; diff --git a/src/commands/color.ts b/src/commands/color.ts index 117156b..3cbcf9c 100644 --- a/src/commands/color.ts +++ b/src/commands/color.ts @@ -1,6 +1,6 @@ import { RoleCreateOptions, GuildMember, CommandInteraction, EmbedBuilder, TextChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle, SlashCommandBuilder } from 'discord.js'; import { default as parseColor, Color } from 'parse-color'; -import { isColorRole, COLOR_ROLE_SEPERATOR } from '../lib/assignableRoles'; +import { getRoleSeperator, isColorRole } from '../lib/assignableRoles'; import * as log from '../lib/log'; import { Command } from '../types/index'; @@ -17,8 +17,8 @@ function removeAllColorRoles(member: GuildMember) { async function applyColor(member: GuildMember, color: Color) { await removeAllColorRoles(member); - const colorRoleSeperator = await member.guild.roles.fetch(COLOR_ROLE_SEPERATOR); - if (!colorRoleSeperator) log.error('no color role seperator found?!?!'); + const seperator = await getRoleSeperator('color', member.guild); + if (!seperator) log.info('no color role seperator found'); const roleSettings: RoleCreateOptions = { name: color.hex, @@ -27,8 +27,8 @@ async function applyColor(member: GuildMember, color: Color) { mentionable: false, permissions: 0n, }; - if (colorRoleSeperator) { - roleSettings.position = colorRoleSeperator.position; + if (seperator) { + roleSettings.position = seperator.position; } const role = member.guild.roles.cache.find(role => role.name === color.hex) || await member.guild.roles.create(roleSettings); @@ -46,7 +46,8 @@ export default { .addBooleanOption((option) => option.setName('preview').setDescription('Preview the color instead of applying it') ) - .setDefaultMemberPermissions(0), + .setDefaultMemberPermissions(0) + .setDMPermission(false), execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; diff --git a/src/commands/garbagecollectroles.ts b/src/commands/garbagecollectroles.ts index aa4018c..d890d60 100644 --- a/src/commands/garbagecollectroles.ts +++ b/src/commands/garbagecollectroles.ts @@ -1,6 +1,5 @@ -import { Guild, GuildMember, CommandInteraction, Role, SlashCommandBuilder } from 'discord.js'; +import { Guild, CommandInteraction, Role, SlashCommandBuilder } from 'discord.js'; import { isColorRole, isPronounRole } from '../lib/assignableRoles'; -import { knownServers } from '../lib/knownServers'; import { Command } from '../types/index'; async function fetchRoleMembers(role: Role) { @@ -25,11 +24,10 @@ async function garbageCollectRoles(guild: Guild, dryRun: boolean): Promise option.setName('dry-run').setDescription('Only show roles that would be deleted.')) - .setDefaultMemberPermissions('0'), - - serverWhitelist: [...knownServers.firepit], + .setDescription('[ADMIN] Garbage collect unused self-assignable roles') + .addBooleanOption((option) => option.setName('dry-run').setDescription('Only show roles that would be deleted')) + .setDefaultMemberPermissions(0) + .setDMPermission(false), execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; diff --git a/src/commands/pronouns.ts b/src/commands/pronouns.ts index 2cb7077..bb9a743 100644 --- a/src/commands/pronouns.ts +++ b/src/commands/pronouns.ts @@ -1,5 +1,5 @@ import { RoleCreateOptions, GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; -import { pronouns, PRONOUN_ROLE_SEPERATOR } from '../lib/assignableRoles'; +import { getRoleSeperator, pronouns } from '../lib/assignableRoles'; import * as log from '../lib/log'; import { Command } from '../types/index'; @@ -18,7 +18,8 @@ export default { .setRequired(true) .setChoices(...Array.from(pronouns.values()).map(extendOption)) ) - .setDefaultMemberPermissions(0), + .setDefaultMemberPermissions(0) + .setDMPermission(false), execute: async (interaction: CommandInteraction) => { if (!interaction.isChatInputCommand()) return; @@ -31,8 +32,8 @@ export default { const pronoun = interaction.options.getString('pronoun', true); - const pronounRoleSeperator = await member.guild.roles.fetch(PRONOUN_ROLE_SEPERATOR); - if (!pronounRoleSeperator) log.error('no pronoun role seperator found?!?!'); + const seperator = await getRoleSeperator('color', member.guild); + if (!seperator) log.info('no color role seperator found'); const roleSettings: RoleCreateOptions = { name: pronoun, @@ -40,8 +41,8 @@ export default { mentionable: false, permissions: 0n }; - if (pronounRoleSeperator) { - roleSettings.position = pronounRoleSeperator.position; + if (seperator) { + roleSettings.position = seperator.position; } const pronounRole = member.guild.roles.cache.find(role => role.name === pronoun) || await member.guild.roles.create(roleSettings); diff --git a/src/commands/roles.ts b/src/commands/roles.ts new file mode 100644 index 0000000..645687c --- /dev/null +++ b/src/commands/roles.ts @@ -0,0 +1,79 @@ +import { GuildMember, CommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { Command } from '../types/index'; +import { RoleSeperator, RoleSeperatorType, db } from '../lib/db'; + +function extendOption(t: string) { + return {name: t, value: t}; +} + +export default { + data: new SlashCommandBuilder() + .setName('roles') + .setDescription('[ADMIN] Manage self-assignable roles') + .addSubcommand(sub => + sub + .setName('seperator') + .setDescription('[ADMIN] Determine where new roles will be created') + .addStringOption(opt => + opt + .setName('type') + .setDescription('The type of self-assignable role') + .setChoices(...['color', 'pronoun'].map(extendOption)) + .setRequired(true) + ) + .addRoleOption(opt => + opt + .setName('seperator') + .setDescription('Roles will be put *below* this role; omit to put them at the bottom of the wole list') + ) + ) + .setDefaultMemberPermissions(0) + .setDMPermission(false), + + execute: async (interaction: CommandInteraction) => { + if (!interaction.isChatInputCommand()) return; + + const member = interaction.member! as GuildMember; + + await interaction.deferReply({ + ephemeral: true + }); + + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'seperator') { + const type = interaction.options.getString('type', true) as RoleSeperatorType; + const seperator = interaction.options.getRole('seperator'); + + if (seperator) { + const highestRole = member.guild.members.me!.roles.highest; + + if (seperator.position > highestRole.position) + return interaction.followUp(`This role is above my highest role (${highestRole.toString()}), so I can\`t put roles up there!`); + + await db('roleSeperators') + .insert({ + guild: member.guild.id, + type, + role: seperator.id, + }); + + await interaction.followUp(`New ${type} roles will be placed below ${seperator.toString()}.`); + } else { + const role = await db('roleSeperators') + .where('guild', member.guild.id) + .where('type', type) + .returning('*') + .delete(); + + if (role === 0) { + await interaction.followUp(`\`${type}\` never had a seperator role in this server!`); + } else { + await interaction.followUp(`Unset seperator role for \`${type}\`. Roles will now be placed at the bottom of the role list.`); + } + } + } else { + // ... + } + } +} satisfies Command; \ No newline at end of file diff --git a/src/lib/assignableRoles.ts b/src/lib/assignableRoles.ts index 7d2e602..49d711c 100644 --- a/src/lib/assignableRoles.ts +++ b/src/lib/assignableRoles.ts @@ -1,5 +1,5 @@ -export const COLOR_ROLE_SEPERATOR = '997893394850381834'; // put color roles below this role -export const PRONOUN_ROLE_SEPERATOR = '997893842961440779'; // put pronoun roles below this role +import { Guild } from 'discord.js'; +import { RoleSeperator, RoleSeperatorType, db } from './db'; export const pronouns = new Set([ 'he/him', @@ -19,4 +19,15 @@ export function isColorRole(name: string) { export function isPronounRole(name: string) { return pronouns.has(name); +} + +export async function getRoleSeperator(type: RoleSeperatorType, guild: Guild) { + const seperator = await db('roleSeperators') + .select('role') + .where('guild', guild.id) + .where('type', type) + .first(); + + if (seperator) return await guild.roles.fetch(seperator.role); + return null; } \ No newline at end of file diff --git a/src/lib/db.ts b/src/lib/db.ts index e55e449..69d7cb1 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -96,4 +96,10 @@ export interface ItemBehavior { item: number, behavior: string, value?: number +} +export type RoleSeperatorType = 'color' | 'pronoun'; +export interface RoleSeperator { + guild: string, + role: string, + type: RoleSeperatorType, } \ No newline at end of file