import { Client, CommandInteraction, GuildMember, EmbedBuilder, TextChannel, AutocompleteInteraction } from 'discord.js'; import { getSign } from './util'; import { Counter, CounterConfiguration, CounterUserLink, db } from './db'; export async function getCounter(type: string) { const counter = await db('counters') .where('key', type) .first(); if (!counter) throw 'No such counter'; return counter.value; } export async function changeCounter(delta: number, type: string) { const value = await getCounter(type); const newValue = value + delta; await db('counters') .where('key', type) .update({ 'value': newValue }); return newValue; } export async function getCounterData(type: string) { const counter = await db('counters') .select('*') .where('key', type) .first(); if (!counter) throw 'No such counter'; return counter; } export async function getCounterConfigRaw(type: string, counter: Counter) { const configs = await db('counterConfigurations') .select('configName', 'value') .where('counter', type); const config = new Map(); configs.forEach(({ configName, value }) => { config.set(configName, value); }); // just the ugly way of life config.set('emoji', counter.emoji); config.set('messageTemplate', counter.messageTemplate || (counterConfigs.get('messageTemplate')!.default! as string)); // wow! this line is truly awful return config; } export async function getCounterConfig(type: string, key: string) { const config = await db('counterConfigurations') .select('value') .where('counter', type) .first(); const valueStr = config?.value; let value; if (valueStr) { value = parseConfig(valueStr, counterConfigs.get(key)!.type); } else { value = counterConfigs.get(key)!.default; } return value; } export async function setCounterConfig(type: string, option: string, value: string) { // just the ugly way of life if (option === 'emoji') { await db('counters') .update({ 'emoji': value }); return; } if (option === 'messageTemplate') { await db('counters') .update({ 'messageTemplate': value }); return; } const updated = await db('counterConfigurations') .update({ value: value }) .where('counter', type) .where('configName', option); if (updated === 0) { await db('counterConfigurations') .insert({ 'counter': type, 'guild': '0', //TODO 'configName': option, 'value': value }); } } export enum ConfigType { Bool, String, Number } export function parseConfig(str: string, type: ConfigType.Bool): boolean export function parseConfig(str: string, type: ConfigType.String): string export function parseConfig(str: string, type: ConfigType.Number): number export function parseConfig(str: string, type: ConfigType): boolean | string | number export function parseConfig(str: string, type: ConfigType) { switch(type) { case ConfigType.Bool: return str === 'true'; case ConfigType.String: return str.trim(); case ConfigType.Number: { const n = parseInt(str); if (isNaN(n)) throw 'Not a valid number'; return n; } } } export function getOptions(type: ConfigType): string[] { switch(type) { case ConfigType.Bool: return ['true', 'false']; case ConfigType.String: return []; case ConfigType.Number: return []; } } export function toStringConfig(value: boolean | string | number, type: ConfigType): string { switch(type) { case ConfigType.Bool: return value ? 'true' : 'false'; case ConfigType.String: return (value as string); case ConfigType.Number: return (value as number).toString(); } } export const counterConfigs = new Map([ ['anonymous', { type: ConfigType.Bool, default: false }], // these ones are fake and are just stand-ins for values defined inside the actual counters table ['emoji', { type: ConfigType.String, default: '' }], ['messageTemplate', { type: ConfigType.String, default: '**%user** has %action the counter by **%amt**.' }] ]); export async function updateCounter(bot: Client, counter: Counter, value: number) { const channel = await bot.channels.fetch(counter.channel) as TextChannel; const messageID = counter.message; const content = `[${counter.emoji}] x${value}`; // bit janky // yeah you don't say try { if (messageID) { const message = await channel.messages.fetch(messageID); if (!message) throw new Error(); await message.edit(content); } else { throw new Error(); } } catch(err) { const message = await channel.send(content); message.pin(); await db('counters') .where('key', counter.key) .update({ 'message': message.id }); } } export async function announceCounterUpdate(bot: Client, member: GuildMember, delta: number, counter: Counter, value: number) { const channel = await bot.channels.fetch(counter.channel) as TextChannel; const template = counter.messageTemplate || counterConfigs.get('messageTemplate')!.default as string; const anonymous = await getCounterConfig(counter.key, 'anonymous'); const embed = new EmbedBuilder() //.setDescription(`**${member.toString()}** has ${delta > 0 ? 'increased' : 'decreased'} the counter by **${Math.abs(delta)}**.`) .setDescription( template .replaceAll('%user', anonymous ? 'someone' : member.toString()) .replaceAll('%action', delta > 0 ? 'increased' : 'decreased') .replaceAll('%amt', Math.abs(delta).toString()) ) .setTimestamp() .setFooter({ text: `[${counter.emoji}] x${value}` }); if (!anonymous) { embed .setAuthor({ name: member.displayName, iconURL: member.displayAvatarURL() }) .setColor(member.displayColor); } await channel.send({ embeds: [embed] }); } export async function changeCounterInteraction(interaction: CommandInteraction, member: GuildMember, amount: number, type: string) { 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); 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) { await interaction.followUp({ content: (err as Error).toString() }); } } export async function counterAutocomplete(interaction: AutocompleteInteraction) { const focusedValue = interaction.options.getFocused(); const guild = interaction.guildId; const query = db('counters') .select('emoji', 'key') .whereLike('key', `%${focusedValue.toLowerCase()}%`) .limit(25); if (guild) { query.where('guild', guild); } const foundCounters = await query; await interaction.respond( foundCounters.map(choice => ({ name: choice.emoji, value: choice.key })) ); }