import { AutocompleteInteraction, CommandInteraction, SlashCommandBuilder } from 'discord.js'; import { Counter, CounterUserLink, db } from '../lib/db'; import { counterAutocomplete, counterConfigs, findCounter, getCounterConfigRaw, getOptions, parseConfig, setCounterConfig, toStringConfig, updateCounter } from '../lib/rpg/counter'; import { outdent } from 'outdent'; import { formatItem, formatItems, getItem, itemAutocomplete } from '../lib/rpg/items'; import { Command } from '../types/index'; import { set } from '../lib/autocomplete'; function extendOption(t: string) { return {name: t, value: t}; } const help = new Map([ ['message templates', outdent` When using \`messageTemplate\`, \`messageTemplateIncrease\`, \`messageTemplateDecrease\`, \`messageTemplatePut\` or \`messageTemplateTake\`, you are providing a **template string**. A template string is a **specially-formatted** string with placeholder values. For instance, a template string like so: > **%user** has %action the counter by **%amt**. Could be formatted as such: > **@oatmealine** has incremented the counter by **1**. Here are the keys you can use as special replacement values: - \`%user\` - The user that changed the counter - \`%action\` - "decremented" or "incremented" - \`%amt\` - Amount by which the counter has changed - \`%total\` - The new counter value `] ]); export default { 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('Give your counter a simple name') .setRequired(true) ) .addStringOption(option => option .setName('emoji') .setDescription('An emoji or symbol or something to represent the counter') .setRequired(true) .setMaxLength(100) ) .addNumberOption(option => option .setName('value') .setDescription('Initial value to start with') ) ) .addSubcommand(sub => sub .setName('set') .setDescription('[ADMIN] Configure a counter') .addStringOption(opt => opt .setName('type') .setDescription('The counter to operate on') .setRequired(true) .setAutocomplete(true) ) .addStringOption(opt => opt .setName('key') .setDescription('The config name') .setRequired(true) .setChoices(...[...counterConfigs.keys()].map(extendOption)) ) .addStringOption(opt => opt .setName('value') .setDescription('The new value') .setRequired(true) .setAutocomplete(true) .setMaxLength(100) ) ) .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) ) ) .addSubcommand(sub => sub .setName('list') .setDescription('[ADMIN] List every counter in this server') ) .addSubcommand(sub => sub .setName('help') .setDescription('Help guides for working with counters') .addStringOption(opt => opt .setName('topic') .setDescription('The topic to get help on') .setRequired(true) .setChoices(...[...help.keys()].map(extendOption)) ) ) .addSubcommand(sub => sub .setName('link') .setDescription('[ADMIN] THIS IS IRREVERSIBLE! Attach an item to this counter, letting you take or put items in.') .addStringOption(opt => opt .setName('type') .setDescription('The counter to operate on') .setRequired(true) .setAutocomplete(true) ) .addStringOption(opt => opt .setName('item') .setDescription('The item') .setAutocomplete(true) .setRequired(true) ) ) .setDefaultMemberPermissions('0') .setDMPermission(false), execute: async (interaction: CommandInteraction) => { 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 findCounter(type, interaction.guildId!); } catch(err) { return interaction.followUp('No such counter!'); } if (subcommand === 'add') { const user = interaction.options.getUser('user', true); const userType = interaction.options.getString('usertype', true); const link = await db('counterUserLink') .where('id', counter.id) .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({ 'id': counter.id, '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('id', counter.id) .where('user', user.id) .where('producer', userType === 'producer') .first(); if (!link) return interaction.followUp(`<@${user.id}> is not in the ${counter.emoji} **${userType}** allowlist!`); 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('id', counter.id) .update({ 'allowlistProducer': enabled }); } else { await db('counters') .where('id', counter.id) .update({ 'allowlistConsumer': 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('id', counter.id) .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!; const [counter] = await db('counters') .insert({ 'key': key, 'emoji': emoji, 'value': value, 'channel': channel.id, 'guild': guild }) .returning('*'); await updateCounter(interaction.client, counter, value); await interaction.followUp({ content: `<#${channel.id}> has been **enriched** with your new counter. Congratulations!` }); } else if (subcommand === 'set') { const type = interaction.options.getString('type')!; let counter; try { counter = await findCounter(type, interaction.guildId!); } catch(err) { return interaction.followUp('No such counter!'); } const config = await getCounterConfigRaw(counter); const key = interaction.options.getString('key', true); const value = interaction.options.getString('value', true); if (key === 'emoji' && counter.linkedItem) return interaction.followUp(`Cannot modify emoji - this counter is linked to ${formatItem(await getItem(counter.linkedItem))}`); const defaultConfig = counterConfigs.get(key); if (!defaultConfig) return interaction.followUp(`No config named \`${key}\` exists!`); const parsedValue = parseConfig(value, defaultConfig.type); const restringedValue = toStringConfig(parsedValue, defaultConfig.type); await setCounterConfig(counter, key, restringedValue); await interaction.followUp(`${counter.emoji} \`${key}\` is now \`${restringedValue}\`. (was \`${config.get(key) || toStringConfig(defaultConfig.default, defaultConfig.type)}\`)`); } else if (subcommand === 'delete') { const type = interaction.options.getString('type')!; let counter; try { counter = await findCounter(type, interaction.guildId!); } catch(err) { return interaction.followUp('No such counter!'); } await db('counters') .where('id', counter.id) .delete(); await db('counterUserLink') .where('id', counter.id) .delete(); await interaction.followUp({ content: `The ${counter.emoji} ${counter.key} counter has been removed. 😭` }); } else if (subcommand === 'list') { const counters = await db('counters') .where('guild', interaction.guildId!); await interaction.followUp(counters.map(c => `${c.emoji} ${c.key}: **${c.value}** <#${c.channel}>`).join('\n')); } else if (subcommand === 'help') { await interaction.followUp(help.get(interaction.options.getString('topic', true))!); } else if (subcommand === 'link') { const type = interaction.options.getString('type', true); const itemID = parseInt(interaction.options.getString('item', true)); let counter; try { counter = await findCounter(type, interaction.guildId!); } catch(err) { return interaction.followUp('No such counter!'); } const item = await getItem(itemID); if (!item) return interaction.followUp('No such item exists!'); if (item.untradable) return interaction.followUp('This item is untradable!'); await db('counters') .where('id', counter.id) .update({ 'linkedItem': item.id, 'emoji': item.emoji, 'key': item.name, 'value': 0 }); await setCounterConfig(counter, 'canIncrement', 'false'); await setCounterConfig(counter, 'canDecrement', 'false'); await setCounterConfig(counter, 'min', '0'); await interaction.followUp(`Done. **The counter has been reset** to ${formatItems(item, 0)}. Users will not be able to take out or put in items until you enable this with \`canTake\` or \`canPut\`.\n\`canIncrement\` and \`canDecrement\` have also been **automatically disabled** and \`min\` has been set to **0**, and you are recommended to keep these values as such if you want to maintain balance in the universe.`); } } }, autocomplete: set({ type: counterAutocomplete, item: itemAutocomplete, value: async (interaction: AutocompleteInteraction) => { const focused = interaction.options.getFocused(); const type = interaction.options.getString('type', true); const counter = await findCounter(type, interaction.guildId!); const config = await getCounterConfigRaw(counter); const key = interaction.options.getString('key'); if (!key) return []; const defaultConfig = counterConfigs.get(key); if (!defaultConfig) return []; const defaultOptions = getOptions(defaultConfig.type); let options = [ { value: `${config.get(key) || toStringConfig(defaultConfig.default, defaultConfig.type)}`, name: `Current: ${config.get(key) || toStringConfig(defaultConfig.default, defaultConfig.type)}`.slice(0, 99) }, { value: `${toStringConfig(defaultConfig.default, defaultConfig.type)}`, name: `Default: ${toStringConfig(defaultConfig.default, defaultConfig.type)}` }, ...defaultOptions.filter(s => s.startsWith(focused)).map(extendOption) ]; if (focused !== '' && !options.find(opt => opt.value === focused)) { options = [ { value: focused, name: focused }, ...options ]; } return options; } }), } satisfies Command;