counter configuration!

This commit is contained in:
Jill 2023-11-13 18:10:33 +03:00
parent 2c3443a639
commit bccf905e68
Signed by: oat
GPG Key ID: 33489AA58A955108
4 changed files with 276 additions and 10 deletions

View File

@ -0,0 +1,22 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema
.createTable('counterConfigurations', table => {
table.string('counter').references('key').inTable('counters').onDelete('CASCADE');
table.string('guild').notNullable();
table.string('configName').notNullable();
table.string('value').nullable();
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema
.dropTable('counterConfigurations');
};

View File

@ -1,6 +1,6 @@
import { Interaction, SlashCommandBuilder } from 'discord.js';
import { AutocompleteInteraction, Interaction, SlashCommandBuilder } from 'discord.js';
import { Counter, CounterUserLink, db } from '../lib/db';
import { counterAutocomplete, getCounterData, updateCounter } from '../lib/counter';
import { counterAutocomplete, counterConfigs, getCounterConfigRaw, getCounterData, getOptions, parseConfig, setCounterConfig, toStringConfig, updateCounter } from '../lib/counter';
function extendOption(t: string) {
return {name: t, value: t};
@ -130,6 +130,7 @@ module.exports = {
.setName('emoji')
.setDescription('An emoji or symbol or something to represent the counter')
.setRequired(true)
.setMaxLength(100)
)
.addNumberOption(option =>
option
@ -137,6 +138,33 @@ module.exports = {
.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')
@ -280,6 +308,32 @@ module.exports = {
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 getCounterData(type);
} catch(err) {
await interaction.followUp({
content: 'No such counter!'
});
return;
}
const config = await getCounterConfigRaw(interaction.options.getString('type') || '', counter);
const key = interaction.options.getString('key', true);
const value = interaction.options.getString('value', true);
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(type, 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')!;
@ -308,5 +362,44 @@ module.exports = {
}
},
autocomplete: counterAutocomplete
autocomplete: async (interaction: AutocompleteInteraction) => {{
const focused = interaction.options.getFocused(true);
if (focused.name === 'type') {
return counterAutocomplete(interaction);
} else if (focused.name === 'value') {
const type = interaction.options.getString('type', true);
const counter = await getCounterData(type);
const config = await getCounterConfigRaw(type, counter);
const key = interaction.options.getString('key');
if (!key) return interaction.respond([]);
const defaultConfig = counterConfigs.get(key);
if (!defaultConfig) return interaction.respond([]);
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)
},
...defaultOptions.filter(s => s.startsWith(focused.value)).map(extendOption)
];
if (focused.value !== '' && !options.find(opt => opt.value === focused.value)) {
options = [
{
value: focused.value,
name: focused.value
},
...options
];
}
await interaction.respond(options);
}
}}
};

View File

@ -1,6 +1,6 @@
import { Client, CommandInteraction, GuildMember, EmbedBuilder, TextChannel, AutocompleteInteraction } from 'discord.js';
import { getSign } from './util';
import { Counter, CounterUserLink, db } from './db';
import { Counter, CounterConfiguration, CounterUserLink, db } from './db';
export async function getCounter(type: string) {
const counter = await db<Counter>('counters')
@ -36,6 +36,138 @@ export async function getCounterData(type: string) {
return counter;
}
export async function getCounterConfigRaw(type: string, counter: Counter) {
const configs = await db<CounterConfiguration>('counterConfigurations')
.select('configName', 'value')
.where('counter', type);
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);
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<CounterConfiguration>('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<Counter>('counters')
.update({
'emoji': value
});
return;
}
if (option === 'messageTemplate') {
await db<Counter>('counters')
.update({
'messageTemplate': value
});
return;
}
const updated = await db<CounterConfiguration>('counterConfigurations')
.update({
value: value
})
.where('counter', type)
.where('configName', option);
if (updated === 0) {
await db<CounterConfiguration>('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;
@ -67,18 +199,31 @@ export async function updateCounter(bot: Client, counter: Counter, value: number
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()
.setAuthor({
name: `${member.user.username}#${member.user.discriminator}`,
iconURL: member.user.displayAvatarURL()
})
.setDescription(`**${member.toString()}** has ${delta > 0 ? 'increased' : 'decreased'} the counter by **${Math.abs(delta)}**.`)
.setColor(member.displayColor)
//.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.user.username}#${member.user.discriminator}`,
iconURL: member.user.displayAvatarURL()
})
.setColor(member.displayColor);
}
await channel.send({
embeds: [embed]
});

View File

@ -39,4 +39,10 @@ export interface CounterUserLink {
key: string,
user: string,
producer: boolean
}
export interface CounterConfiguration {
counter: string,
guild: string,
configName: string,
value: string
}