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(id: number) { const counter = await db('counters') .select('value') .where('id', id) .first(); if (!counter) throw 'No such counter'; return counter.value; } export async function changeCounter(id: number, delta: number) { const value = await getCounter(id); const newValue = value + delta; await db('counters') .where('id', id) .update({ 'value': newValue }); return newValue; } export async function getCounterData(id: number) { const counter = await db('counters') .select('*') .where('id', id) .first(); if (!counter) throw 'No such counter'; return counter; } export async function findCounter(key: string, guild: string) { const counter = await db('counters') .select('*') .where('key', key) .where('guild', guild) .first(); if (!counter) throw 'No such counter'; return counter; } export async function getCounterConfigRaw(counter: Counter) { const configs = await db('counterConfigurations') .select('configName', 'value') .where('id', counter.id); const config = new Map(); configs.forEach(({ configName, value }) => { config.set(configName, value); }); // just the ugly way of life config.set('emoji', counter.emoji); return config; } export async function getCounterConfig(id: number, key: string) { const config = await db('counterConfigurations') .select('value') .where('id', id) .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(id: number, option: string, value: string) { // just the ugly way of life if (option === 'emoji') { await db('counters') .where('id', id) .update({ 'emoji': value }); return; } const updated = await db('counterConfigurations') .update({ value: value }) .where('id', id) .where('configName', option); if (updated === 0) { await db('counterConfigurations') .insert({ 'id': id, '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 }], ['messageTemplate', { type: ConfigType.String, default: '**%user** has %action the counter by **%amt**.' }], ['messageTemplateIncrease', { type: ConfigType.String, default: 'null' }], ['messageTemplateDecrease', { type: ConfigType.String, default: 'null' }], // these ones are fake and are just stand-ins for values defined inside the actual counters table ['emoji', { type: ConfigType.String, default: '' }] ]); 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('id', counter.id) .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; let template = await getCounterConfig(counter.id, 'messageTemplate') as string; const templateIncrease = await getCounterConfig(counter.id, 'messageTemplateIncrease') as string; if (templateIncrease !== 'null' && delta > 0) template = templateIncrease; const templateDecrease = await getCounterConfig(counter.id, 'messageTemplateDecrease') as string; if (templateDecrease !== 'null' && delta < 0) template = templateDecrease; const anonymous = await getCounterConfig(counter.id, 'anonymous') as boolean; 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()) .replaceAll('%total', value.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 findCounter(type, member.guild.id); let canUse = true; if (amount > 0 && counter.allowlistProducer) { const userLink = await db('counterUserLink') .where('id', counter.id) .where('user', member.id) .where('producer', true) .first(); if (!userLink) canUse = false; } if (amount < 0 && counter.allowlistConsumer) { const userLink = await db('counterUserLink') .where('id', counter.id) .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(counter.id, amount); 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 })) ); }