jillo-bot/src/commands/counter.ts

490 lines
17 KiB
TypeScript

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 { autocomplete, 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>('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>('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>('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<Counter>('counters')
.where('id', counter.id)
.update({
'allowlistProducer': enabled
});
} else {
await db<Counter>('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>('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<Counter>('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<Counter>('counters')
.where('id', counter.id)
.delete();
await db<CounterUserLink>('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<Counter>('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<Counter>('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: 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;