From 77f6164e3686f9c0736c17a448293e1a9b515d5b Mon Sep 17 00:00:00 2001 From: "Jill \"oatmealine\" Monoids" Date: Sun, 12 Nov 2023 22:44:03 +0300 Subject: [PATCH] counters interface refactor complete, allowlists done --- migrations/20231112182746_counters.js | 1 - src/commands/counter.ts | 313 ++++++++++++++++++++++++++ src/commands/createcounter.ts | 68 ------ src/lib/counter.ts | 39 +++- src/lib/db.ts | 1 - 5 files changed, 346 insertions(+), 76 deletions(-) create mode 100644 src/commands/counter.ts delete mode 100644 src/commands/createcounter.ts diff --git a/migrations/20231112182746_counters.js b/migrations/20231112182746_counters.js index 9ff59f2..8df6baf 100644 --- a/migrations/20231112182746_counters.js +++ b/migrations/20231112182746_counters.js @@ -6,7 +6,6 @@ exports.up = function(knex) { return knex.schema .createTable('counters', table => { table.string('key').notNullable(); - table.string('name').notNullable(); table.string('emoji').notNullable(); table.integer('value').defaultTo(0); table.string('channel').notNullable(); diff --git a/src/commands/counter.ts b/src/commands/counter.ts new file mode 100644 index 0000000..df10207 --- /dev/null +++ b/src/commands/counter.ts @@ -0,0 +1,313 @@ +import { Interaction, SlashCommandBuilder } from 'discord.js'; +import { Counter, CounterUserLink, db } from '../lib/db'; +import { counterAutocomplete, getCounterData, updateCounter } from '../lib/counter'; + +function extendOption(t: string) { + return {name: t, value: t}; +} + +module.exports = { + data: new SlashCommandBuilder() + .setName('counter') + .setDescription('[ADMIN] Counter management') + .addSubcommandGroup(grp => + grp + .setName('allowlist') + .setDescription('[ADMIN] Counter allowlist management') + .addSubcommand(sub => + sub + .setName('add') + .setDescription('[ADMIN] Add a user to the allowlist') + .addStringOption(opt => + opt + .setName('type') + .setDescription('The counter to operate on') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption(opt => + opt + .setName('usertype') + .setDescription('Type of user in this predicament') + .setChoices(...['consumer', 'producer'].map(extendOption)) + .setRequired(true) + ) + .addUserOption(opt => + opt + .setName('user') + .setDescription('The user to add') + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub + .setName('remove') + .setDescription('[ADMIN] Remove a user from the allowlist') + .addStringOption(opt => + opt + .setName('type') + .setDescription('The counter to operate on') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption(opt => + opt + .setName('usertype') + .setDescription('Type of user in this predicament') + .setChoices(...['consumer', 'producer'].map(extendOption)) + .setRequired(true) + ) + .addUserOption(opt => + opt + .setName('user') + .setDescription('The user to remove') + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub + .setName('toggle') + .setDescription('[ADMIN] Enable or disable the allowlist.') + .addStringOption(opt => + opt + .setName('type') + .setDescription('The counter to operate on') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption(opt => + opt + .setName('usertype') + .setDescription('Type of user in this predicament') + .setChoices(...['consumer', 'producer'].map(extendOption)) + .setRequired(true) + ) + .addBooleanOption(opt => + opt + .setName('enabled') + .setDescription('Enable or disable the allowlist') + .setRequired(true) + ) + ) + .addSubcommand(sub => + sub + .setName('list') + .setDescription('[ADMIN] List people in the allowlist') + .addStringOption(opt => + opt + .setName('type') + .setDescription('The counter to operate on') + .setRequired(true) + .setAutocomplete(true) + ) + .addStringOption(opt => + opt + .setName('usertype') + .setDescription('Type of user in this predicament') + .setChoices(...['consumer', 'producer'].map(extendOption)) + .setRequired(true) + ) + ) + ) + .addSubcommand(sub => + sub + .setName('create') + .setDescription('[ADMIN] Create a counter') + .addChannelOption(option => + option + .setName('channel') + .setDescription('Channel to put updates into') + .setRequired(true) + ) + .addStringOption(option => + option + .setName('key') + .setDescription('The codename. Best to leave descriptive for later; used in searching for counters') + .setRequired(true) + ) + .addStringOption(option => + option + .setName('emoji') + .setDescription('An emoji or symbol or something to represent the counter') + .setMaxLength(32) + .setRequired(true) + ) + .addNumberOption(option => + option + .setName('value') + .setDescription('Initial value to start with') + ) + ) + .addSubcommand(sub => + sub + .setName('delete') + .setDescription('[ADMIN] Delete a counter') + .addStringOption(opt => + opt + .setName('type') + .setDescription('The counter to operate on') + .setRequired(true) + .setAutocomplete(true) + ) + ) + .setDefaultMemberPermissions('0') + .setDMPermission(false), + + execute: async (interaction: Interaction) => { + if (!interaction.isChatInputCommand()) return; + + await interaction.deferReply({ephemeral: true}); + + const subcommand = interaction.options.getSubcommand(true); + const group = interaction.options.getSubcommandGroup(); + + if (group === 'allowlist') { + const type = interaction.options.getString('type')!; + + let counter; + try { + counter = await getCounterData(type); + } catch(err) { + await interaction.followUp({ + content: 'No such counter!' + }); + return; + } + + if (subcommand === 'add') { + const user = interaction.options.getUser('user', true); + const userType = interaction.options.getString('usertype', true); + + const link = await db('counterUserLink') + .where('key', type) + .where('user', user.id) + .where('producer', userType === 'producer') + .first(); + + if (link) { + await interaction.followUp({ + content: `<@${user.id}> is already in the ${counter.emoji} **${userType}** allowlist!` + }); + return; + } + + await db('counterUserLink') + .insert({ + 'key': type, + 'user': user.id, + 'producer': userType === 'producer' + }); + + await interaction.followUp({ + content: `<@${user.id}> added to the ${counter.emoji} **${userType}** allowlist.` + }); + } else if (subcommand === 'remove') { + const user = interaction.options.getUser('user', true); + const userType = interaction.options.getString('usertype', true); + + const link = await db('counterUserLink') + .where('key', type) + .where('user', user.id) + .where('producer', userType === 'producer') + .first(); + + if (!link) { + await interaction.followUp({ + content: `<@${user.id}> is not in the ${counter.emoji} **${userType}** allowlist!` + }); + return; + } + + await interaction.followUp({ + content: `<@${user.id}> has been removed from the ${counter.emoji} **${userType}** allowlist.` + }); + } else if (subcommand === 'toggle') { + const enabled = interaction.options.getBoolean('enabled', true); + const userType = interaction.options.getString('usertype', true); + + if (userType === 'producer') { + await db('counters') + .where('key', type) + .update({ + 'allowlistProducer': enabled + }); + } else { + await db('counters') + .where('key', type) + .update({ + 'allowlistProducer': enabled + }); + } + + await interaction.followUp({ + content: `${counter.emoji} ${userType.slice(0, 1).toUpperCase() + userType.slice(1)} allowlist is now **${enabled ? 'enabled' : 'disabled'}**.` + }); + } else if (subcommand === 'list') { + const userType = interaction.options.getString('usertype', true); + + const users = await db('counterUserLink') + .where('key', type) + .where('producer', userType === 'producer'); + + const enabled = (userType === 'producer') ? counter.allowlistProducer : counter.allowlistConsumer; + + await interaction.followUp({ + content: `${counter.emoji} ${userType.slice(0, 1).toUpperCase() + userType.slice(1)}s:\n${users.map(u => `- <@${u.user}>`)}\nThe ${userType} allowlist is currently **${enabled ? 'enabled' : 'disabled'}**.` + }); + } + } else { + if (subcommand === 'create') { + const channel = interaction.options.getChannel('channel', true); + const key = interaction.options.getString('key', true); + const emoji = interaction.options.getString('emoji', true); + const value = interaction.options.getNumber('value') || 0; + const guild = interaction.guildId!; + + await db('counters') + .insert({ + 'key': key, + 'emoji': emoji, + 'value': value, + 'channel': channel.id, + 'guild': guild + }); + + const counter = await db('counters') + .where('key', key) + .first(); + + await updateCounter(interaction.client, counter!, value); + + await interaction.followUp({ + content: `<#${channel.id}> has been **enriched** with your new counter. Congratulations!` + }); + } else if (subcommand === 'delete') { + const type = interaction.options.getString('type')!; + + let counter; + try { + counter = await getCounterData(type); + } catch(err) { + await interaction.followUp({ + content: 'No such counter!' + }); + return; + } + + await db('counters') + .where('key', type) + .delete(); + + await db('counterUserLink') + .where('key', type) + .delete(); + + await interaction.followUp({ + content: `The ${counter.emoji} counter has been removed. 😭` + }); + } + } + }, + + autocomplete: counterAutocomplete +}; \ No newline at end of file diff --git a/src/commands/createcounter.ts b/src/commands/createcounter.ts deleted file mode 100644 index 58627d1..0000000 --- a/src/commands/createcounter.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Interaction, SlashCommandBuilder } from 'discord.js'; -import { Counter, db } from '../lib/db'; -import { updateCounter } from '../lib/counter'; - -module.exports = { - data: new SlashCommandBuilder() - .setName('createcounter') - .setDescription('[ADMIN] Create a counter in this channel') - .addStringOption(option => - option - .setName('key') - .setDescription('The codename. Best to leave descriptive for later') - .setRequired(true) - ) - .addStringOption(option => - option - .setName('name') - .setDescription('The name, anything goes') - .setRequired(true) - ) - .addStringOption(option => - option - .setName('emoji') - .setDescription('An emoji or symbol or something to represent the counter') - .setMaxLength(10) - .setRequired(true) - ) - .addNumberOption(option => - option - .setName('value') - .setDescription('Initial value to start with') - ) - .setDefaultMemberPermissions('0') - .setDMPermission(false), - - execute: async (interaction: Interaction) => { - if (!interaction.isChatInputCommand()) return; - - await interaction.deferReply({ephemeral: true}); - - const key = interaction.options.getString('key')!; - const name = interaction.options.getString('name')!; - const emoji = interaction.options.getString('emoji')!; - const value = interaction.options.getNumber('value') || 0; - const channel = interaction.channelId; - const guild = interaction.guildId!; - - await db('counters') - .insert({ - 'key': key, - 'name': name, - 'emoji': emoji, - 'value': value, - 'channel': channel, - 'guild': guild - }); - - const counter = await db('counters') - .where('key', key) - .first(); - - await updateCounter(interaction.client, counter!, value); - - await interaction.followUp({ - content: `<#${interaction.channelId}> has been **enriched** with your new counter. Congratulations!` - }); - } -}; \ No newline at end of file diff --git a/src/lib/counter.ts b/src/lib/counter.ts index 31ced73..c8ff077 100644 --- a/src/lib/counter.ts +++ b/src/lib/counter.ts @@ -1,6 +1,6 @@ import { Client, CommandInteraction, GuildMember, EmbedBuilder, TextChannel, AutocompleteInteraction } from 'discord.js'; import { getSign } from './util'; -import { Counter, db } from './db'; +import { Counter, CounterUserLink, db } from './db'; export async function getCounter(type: string) { const counter = await db('counters') @@ -88,14 +88,41 @@ export async function changeCounterInteraction(interaction: CommandInteraction, try { const counter = await getCounterData(type); + let canUse = true; + if (amount > 0 && counter.allowlistProducer) { + const userLink = await db('counterUserLink') + .where('key', type) + .where('user', member.id) + .where('producer', true) + .first(); + + if (!userLink) canUse = false; + } + if (amount < 0 && counter.allowlistConsumer) { + const userLink = await db('counterUserLink') + .where('key', type) + .where('user', member.id) + .where('producer', false) + .first(); + + if (!userLink) canUse = false; + } + + if (!canUse) { + await interaction.followUp({ + content: `You cannot **${amount > 0 ? 'produce' : 'consume'}** ${counter.emoji}.` + }); + return; + } + const newCount = await changeCounter(amount, type); await updateCounter(interaction.client, counter, newCount); await announceCounterUpdate(interaction.client, member, amount, counter, newCount); - interaction.followUp({ + await interaction.followUp({ content: `${counter.emoji} **You have ${amount > 0 ? 'increased' : 'decreased'} the counter.**\n\`\`\`diff\n ${newCount - amount}\n${getSign(amount)}${Math.abs(amount)}\n ${newCount}\`\`\`` }); } catch(err) { - interaction.followUp({ + await interaction.followUp({ content: (err as Error).toString() }); } @@ -106,8 +133,8 @@ export async function counterAutocomplete(interaction: AutocompleteInteraction) const guild = interaction.guildId; const query = db('counters') - .select('name', 'key') - .whereLike('name', `%${focusedValue.toLowerCase()}%`) + .select('emoji', 'key') + .whereLike('key', `%${focusedValue.toLowerCase()}%`) .limit(25); if (guild) { @@ -117,6 +144,6 @@ export async function counterAutocomplete(interaction: AutocompleteInteraction) const foundCounters = await query; await interaction.respond( - foundCounters.map(choice => ({ name: choice.name, value: choice.key })) + foundCounters.map(choice => ({ name: choice.emoji, value: choice.key })) ); } \ No newline at end of file diff --git a/src/lib/db.ts b/src/lib/db.ts index 9624ba8..634990a 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -26,7 +26,6 @@ export interface Subscription { } export interface Counter { key: string, - name: string, emoji: string, value: number, channel: string,