jillo-bot/src/lib/counter.ts

314 lines
8.5 KiB
TypeScript

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<Counter>('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<Counter>('counters')
.where('id', id)
.update({
'value': newValue
});
return newValue;
}
export async function getCounterData(id: number) {
const counter = await db<Counter>('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<Counter>('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<CounterConfiguration>('counterConfigurations')
.select('configName', 'value')
.where('id', counter.id);
const config = new Map<string, string>();
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<CounterConfiguration>('counterConfigurations')
.select('value')
.where('id', id)
.where('configName', key)
.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<Counter>('counters')
.where('id', id)
.update({
'emoji': value
});
return;
}
const updated = await db<CounterConfiguration>('counterConfigurations')
.update({
value: value
})
.where('id', id)
.where('configName', option);
if (updated === 0) {
await db<CounterConfiguration>('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<Counter>('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>('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>('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<Counter>('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 }))
);
}