Skip to content

Commit

Permalink
Add role name templating
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchell-merry committed Sep 2, 2023
1 parent 6136ee6 commit addab57
Show file tree
Hide file tree
Showing 12 changed files with 1,686 additions and 33 deletions.
1,506 changes: 1,483 additions & 23 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"docker:push": "npm run docker:build && docker push mitchellmerry/src-wrs-bot-v2",
"registerCommands": "tsc && node --experimental-specifier-resolution=node build/scripts/register_commands.js",
"clearCommands": "tsc && node --experimental-specifier-resolution=node build/scripts/clear_commands.js",
"test": "vitest --config ./vitest.config.ts",
"typeorm": "typeorm-ts-node-esm -d src/db/index.ts"
},
"repository": {
Expand All @@ -40,7 +41,8 @@
"prettier": "3.0.3",
"reflect-metadata": "^0.1.13",
"src-ts": "^2.8.0",
"typeorm": "^0.3.6"
"typeorm": "^0.3.6",
"vitest": "^0.34.3"
},
"devDependencies": {
"@types/node": "^17.0.36",
Expand All @@ -55,4 +57,4 @@
"singleQuote": true,
"arrowParens": "avoid"
}
}
}
6 changes: 6 additions & 0 deletions src/db/entities/Guild.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export class GuildEntity {
@Column({ default: '#FEE75C' })
role_default_colour!: HexColorString;

/** The default role name template */
@Column({
default: '{{game}}: {{level}} - {{category}} ({{subcategories}})',
})
role_default_name!: string;

/** The ID of the role to place new roles above. */
@Column({ default: '' })
above_role_id!: string;
Expand Down
2 changes: 1 addition & 1 deletion src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ if (!synchronize) {
migrations = [join(__dirname, 'migrations', '*.{ts,js}')];
console.log(`Will look for migrations at \`${migrations}\`.`);
} else {
console.log(`Will syncronize database.`);
console.log(`Will synchronize database.`);
}

export const DB = new DataSource({
Expand Down
26 changes: 26 additions & 0 deletions src/db/migrations/1693675771967-RoleDefaultName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { MigrationInterface, QueryFailedError, QueryRunner } from 'typeorm';

export class RoleDefaultName1693675771967 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
try {
await queryRunner.query(
`ALTER TABLE \`guild\` ADD \`role_default_name\` varchar(255) NOT NULL`,
);
await queryRunner.query(
'UPDATE `guild` SET `role_default_name` = ""',
);
} catch (e) {
// TODO LOL!
if (!(e instanceof QueryFailedError)) {
throw e;
}
console.log(`IGNORING: ` + e);
}
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE \`guild\` DROP COLUMN \`role_default_name\``,
);
}
}
19 changes: 14 additions & 5 deletions src/discord/commands/leaderboard/lb.add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ConfirmationMenu from '../../menus/ConfirmationMenu';
import LeaderboardMenu from '../../menus/LeaderboardMenu';
import UserError from '../../UserError';
import { Subcommand } from '../command';
import { templateLeaderboardName } from '../../../lib/template_name';

const gameRegex = /^\w+$/;

Expand Down Expand Up @@ -104,12 +105,19 @@ const LeaderboardAddCommand: Subcommand = {
`This guild is already tracking the leaderboard ${lb_name}.`,
);

const role_name = templateLeaderboardName(guildEnt.role_default_name, {
game: game.names.international,
category: category.name,
subcategories: labels,
level: level?.name,
});

// confirm with the user that this is the action we want to take
const message = roleOpt
? `This will track the leaderboard ${lb_name} in this guild with the role <@&${roleOpt.id}>. Are you sure you wish to do this?`
: guildEnt.above_role_id === ''
? `This will track the leaderboard ${lb_name} in this guild with a new role. Are you sure you wish to do this?`
: `This will track the leaderboard ${lb_name} in this guild with a new role, created above <@&${guildEnt.above_role_id}>. Are you sure you wish to do this?`;
? `This will track the leaderboard ${lb_name} in this guild with a new role with the name ${role_name}. Are you sure you wish to do this?`
: `This will track the leaderboard ${lb_name} in this guild with a new role with the name ${role_name}, created above <@&${guildEnt.above_role_id}>. Are you sure you wish to do this?`;

const [confirmation] = await new ConfirmationMenu(message).spawnMenu(
interaction,
Expand All @@ -119,13 +127,14 @@ const LeaderboardAddCommand: Subcommand = {

let role: Role;
if (roleOpt) role = roleOpt as Role;
else
else {
role = await interaction.guild!.roles.create({
name: `${lb_name} WR`,
name: role_name,
color: guildEnt.role_default_colour,
position,
permissions: [],
});
}

// save new leaderboard in database
if (!board) {
Expand All @@ -150,7 +159,7 @@ const LeaderboardAddCommand: Subcommand = {
await lRepo.save(board);

await interaction.editReply({
content: `Added the leaderboard ${lb_name}.`,
content: `Added the leaderboard ${lb_name}, tracked by <@&${role.id}>.`,
components: [],
});
},
Expand Down
6 changes: 4 additions & 2 deletions src/discord/commands/set/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import { CommandWithSubcommands } from '../command';
import SetAboveRoleCommand from './set.ar';
import SetListCommand from './set.list';
import SetRoleDefaultColourCommand from './set.rdc';
import SetRoleDefaultNameCommand from './set.rdn';
import SetLogChannelCommand from './set.lc';

const SetCommand: CommandWithSubcommands = {
data: new SlashCommandBuilder()
.setName('set')
.setDescription('Set server settings. Requires moderator permissions.'),
subcommands: [
SetAboveRoleCommand,
SetRoleDefaultColourCommand,
SetListCommand,
SetAboveRoleCommand,
SetLogChannelCommand,
SetRoleDefaultColourCommand,
SetRoleDefaultNameCommand,
],
};

Expand Down
1 change: 1 addition & 0 deletions src/discord/commands/set/set.list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const SetListCommand: Subcommand = {
else msg += `<@&${guildEnt.above_role_id}>\n`;

msg += `\`role_default_colour\`: ${guildEnt.role_default_colour}`;
msg += `\`role_default_name\`: ${guildEnt.role_default_name}`;

await interaction.reply({
content: msg,
Expand Down
31 changes: 31 additions & 0 deletions src/discord/commands/set/set.rdn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SlashCommandSubcommandBuilder } from 'discord.js';
import { DB } from '../../../db';
import { GuildEntity } from '../../../db/entities';
import { Subcommand } from '../command';

const SetRoleDefaultNameCommand: Subcommand = {
data: new SlashCommandSubcommandBuilder()
.setName('role_default_name')
.setDescription(
'The default name of new roles created by the bot. Should be a templated string.',
)
.addStringOption(o =>
o
.setName('role_default_name')
.setDescription('The name.')
.setRequired(true),
),
perm: 'mods',
execute: async (interaction, guildEnt) => {
const name = interaction.options.getString('role_default_name', true);

await DB.getRepository(GuildEntity).update(
{ guild_id: guildEnt.guild_id },
{ role_default_name: name },
);

await interaction.reply(`role_default_name set to \`${name}\`.`);
},
};

export default SetRoleDefaultNameCommand;
66 changes: 66 additions & 0 deletions src/lib/template_name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, it, expect } from 'vitest';
import { LeaderboardNameData, templateLeaderboardName } from './template_name';

describe('role name templating', () => {
const cases: [string, LeaderboardNameData, number][] = [
[
'full-game, no sub-categories',
{ game: 'A Game', category: 'A Category' },
0,
],
[
'level, no sub-categories',
{ game: 'A Game', level: 'A Level', category: 'A Category' },
1,
],
[
'full-game, sub-categories',
{
game: 'A Game',
category: 'A Category',
subcategories: ['Value 1', 'Value 2'],
},
2,
],
[
'level, sub-categories',
{
game: 'A Game',
level: 'A Level',
category: 'A Category',
subcategories: ['Value 1', 'Value 2'],
},
3,
],
];

describe.each(cases)('%s', (_name, leaderboard, i) => {
const expected = {
'{{%game%}}{{: %level%}} - {{%category%}}{{ (%subcategories%)}} WR':
[
'A Game - A Category WR',
'A Game: A Level - A Category WR',
'A Game - A Category (Value 1, Value 2) WR',
'A Game: A Level - A Category (Value 1, Value 2) WR',
],
'WR: {{%game%}}{{: %level%}}': [
'WR: A Game',
'WR: A Game: A Level',
'WR: A Game',
'WR: A Game: A Level',
],
'WR: {{%level%: }}{{%category%}}{{ (%subcategories%)}}': [
'WR: A Category',
'WR: A Level: A Category',
'WR: A Category (Value 1, Value 2)',
'WR: A Level: A Category (Value 1, Value 2)',
],
} as const;

it.each(Object.keys(expected))('for template %s', template => {
expect(templateLeaderboardName(template, leaderboard)).toEqual(
expected[template as keyof typeof expected][i],
);
});
});
});
50 changes: 50 additions & 0 deletions src/lib/template_name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
export type LeaderboardNameData = {
game: string;
category: string;
subcategories?: string[];
level?: string;
};

const templateReg = /{{.+?}}/g;
const keyReg = /%.+?%/g;

export function templateLeaderboardName(
name: string,
leaderboard: LeaderboardNameData,
) {
const names = {
...leaderboard,
subcategories: leaderboard.subcategories?.join(', '),
};

let finalName = name;

const templates = name.matchAll(templateReg);
for (const templateStr of templates) {
const keysToTemplate = templateStr[0].matchAll(keyReg);
let replaceWith = templateStr[0].substring(
2,
templateStr[0].length - 2,
);

for (const keyToTemplate of keysToTemplate) {
let rawKey = keyToTemplate[0];
const key = rawKey.substring(
1,
rawKey.length - 1,
) as keyof LeaderboardNameData;

let value = names[key];
if (value === undefined) {
replaceWith = '';
break;
}

replaceWith = replaceWith.replace(keyToTemplate[0], value);
}

finalName = finalName.replace(templateStr[0], replaceWith);
}

return finalName;
}
Empty file added vitest.config.ts
Empty file.

0 comments on commit addab57

Please sign in to comment.