From 4412d728d92c1d2ce421c41ba21d6a5323c0b4f9 Mon Sep 17 00:00:00 2001 From: "mend-for-github-com[bot]" <50673670+mend-for-github-com[bot]@users.noreply.github.com> Date: Fri, 23 Feb 2024 23:11:18 +0000 Subject: [PATCH 1/3] chore(deps): update github/codeql-action action to v3 --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 76a284f..0e3a02e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,7 +37,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -48,7 +48,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -62,4 +62,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 53b72ff5f776808b6cead51271ec3d6b9da61316 Mon Sep 17 00:00:00 2001 From: BenSegal855 Date: Mon, 4 Nov 2024 18:52:31 -0500 Subject: [PATCH 2/3] feat: Start on meetup command --- src/commands/Info/committiee.ts | 59 ++++++++++++++++++++++++++++++ src/lib/mongo.ts | 7 +++- src/lib/types/database/index.d.ts | 1 + src/lib/types/database/meetup.d.ts | 6 +++ 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 src/commands/Info/committiee.ts create mode 100644 src/lib/types/database/meetup.d.ts diff --git a/src/commands/Info/committiee.ts b/src/commands/Info/committiee.ts new file mode 100644 index 0000000..3102f35 --- /dev/null +++ b/src/commands/Info/committiee.ts @@ -0,0 +1,59 @@ +import { ApplyOptions } from '@sapphire/decorators'; +import type { Args } from '@sapphire/framework'; +import { SubcommandOptions } from '@sapphire/plugin-subcommands'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, time, type Message } from 'discord.js'; +import { SteveSubcommand } from '@lib/extensions/SteveSubcommand'; +import { sendLoadingMessage } from '@lib/utils'; + +@ApplyOptions({ + description: 'Find out and log Committee meetups', + preconditions: ['CommitteeOnly'], + detailedDescription: { + usage: 'meetup \nleaderboard', + examples: [ + 'meetup Apr-28-2023 <@696783853267976202>', + 'leaderboard' + ] + }, + subcommands: [ + { + name: 'meetup', + messageRun: 'newMeetup' + }, + { + name: 'leaderboard', + messageRun: 'leaderboard' + } + ] +}) +export class UserCommand extends SteveSubcommand { + + public async newMeetup(msg: Message, args: Args) { + const resp = await sendLoadingMessage(msg); + const date = await args.pick('date'); + const confirmedMembers = [msg.author.id]; + const possibleMembers = [msg.author.id]; + + possibleMembers.push(...await args.repeat('user').then(users => users.map(user => user.id))); + + await this.container.db.meetups.insertOne({ + id: resp.id, + confirmedMembers, + possibleMembers, + date + }); + + resp.edit({ + content: `<@${possibleMembers.join('>, <@')}>, did you all meet up on ${time(date, 'D')}?`, + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel('I was there!') + .setCustomId(`meetup|${resp.id}`) + .setStyle(ButtonStyle.Success) + ) + ] + }); + } + +} diff --git a/src/lib/mongo.ts b/src/lib/mongo.ts index eea2f67..e84dc1c 100644 --- a/src/lib/mongo.ts +++ b/src/lib/mongo.ts @@ -10,7 +10,8 @@ import type { ChannelRename, QuickRoll, RPCharter, - RollImportCharacter + RollImportCharacter, + Meetup } from '@lib/types/database'; export interface SteveCollections { @@ -24,6 +25,7 @@ export interface SteveCollections { quickRolls: Collection; rpCharacters: Collection; rollImportCharacters: Collection; + meetups: Collection } export async function startMongo() { @@ -46,7 +48,8 @@ export async function startMongo() { channelRename: database.collection('channelRename'), quickRolls: database.collection('quickRolls'), rpCharacters: database.collection('rpCharacters'), - rollImportCharacters: database.collection('rollImportCharacters') + rollImportCharacters: database.collection('rollImportCharacters'), + meetups: database.collection('meetups') }; container.mongo = mongo; diff --git a/src/lib/types/database/index.d.ts b/src/lib/types/database/index.d.ts index bc5cc8b..0c2f094 100644 --- a/src/lib/types/database/index.d.ts +++ b/src/lib/types/database/index.d.ts @@ -10,3 +10,4 @@ export * from './quickRoll'; export * from './idHint'; export * from './rpCharacter'; export * from './rollImportCharacter'; +export * from './meetup'; diff --git a/src/lib/types/database/meetup.d.ts b/src/lib/types/database/meetup.d.ts new file mode 100644 index 0000000..d35f7c9 --- /dev/null +++ b/src/lib/types/database/meetup.d.ts @@ -0,0 +1,6 @@ +export type Meetup = { + id: string; + possibleMembers: string[]; + confirmedMembers: string[] + date: Date; +}; From 480e2e15fdf3d84daad884f32947cd32a1ea1827 Mon Sep 17 00:00:00 2001 From: Ben Segal Date: Tue, 5 Nov 2024 16:30:09 -0500 Subject: [PATCH 3/3] feat: finish committiee meetup leaderboard --- .../committieeLeaderboardBackground.svg | 21 ++ src/commands/Info/committiee.ts | 193 ++++++++++++++++-- src/interaction-handlers/committieeMeetup.ts | 51 +++++ 3 files changed, 251 insertions(+), 14 deletions(-) create mode 100644 src/assets/committieeLeaderboardBackground.svg create mode 100644 src/interaction-handlers/committieeMeetup.ts diff --git a/src/assets/committieeLeaderboardBackground.svg b/src/assets/committieeLeaderboardBackground.svg new file mode 100644 index 0000000..713896f --- /dev/null +++ b/src/assets/committieeLeaderboardBackground.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/commands/Info/committiee.ts b/src/commands/Info/committiee.ts index 3102f35..db52b50 100644 --- a/src/commands/Info/committiee.ts +++ b/src/commands/Info/committiee.ts @@ -1,9 +1,11 @@ import { ApplyOptions } from '@sapphire/decorators'; -import type { Args } from '@sapphire/framework'; +import { UserError, type Args, type Command } from '@sapphire/framework'; import { SubcommandOptions } from '@sapphire/plugin-subcommands'; -import { ActionRowBuilder, ButtonBuilder, ButtonStyle, time, type Message } from 'discord.js'; +import { ActionRowBuilder, BaseMessageOptions, ButtonBuilder, ButtonStyle, EmbedBuilder, Guild, time, User, type Message } from 'discord.js'; import { SteveSubcommand } from '@lib/extensions/SteveSubcommand'; import { sendLoadingMessage } from '@lib/utils'; +import { createCanvas, loadImage } from 'canvas'; +import { readFileSync } from 'fs'; @ApplyOptions({ description: 'Find out and log Committee meetups', @@ -18,42 +20,205 @@ import { sendLoadingMessage } from '@lib/utils'; subcommands: [ { name: 'meetup', - messageRun: 'newMeetup' + messageRun: 'messageNewMeetup', + chatInputRun: 'chatInputNewMeetup' }, { name: 'leaderboard', - messageRun: 'leaderboard' + messageRun: 'messageLeaderboard', + chatInputRun: 'chatInputLeaderboard' } ] }) export class UserCommand extends SteveSubcommand { - public async newMeetup(msg: Message, args: Args) { - const resp = await sendLoadingMessage(msg); - const date = await args.pick('date'); - const confirmedMembers = [msg.author.id]; - const possibleMembers = [msg.author.id]; + public override registerApplicationCommands(registry: Command.Registry) { + registry.registerChatInputCommand(builder => { + builder + .setName(this.name) + .setDescription(this.description) + .addSubcommand(subcommand => { + subcommand + .setName('meetup') + .setDescription('Create a new meetup') + .addStringOption(option => option + .setName('date') + .setDescription('The date of the meetup (Mon-DD-YYYY)') + .setRequired(true) + ); - possibleMembers.push(...await args.repeat('user').then(users => users.map(user => user.id))); + for (let i = 1; i <= 10; i++) { + subcommand.addUserOption(option => option.setName(`attendee${i}`).setDescription(`Attendee ${i}`).setRequired(i === 1)); + } + return subcommand; + }) + .addSubcommand(subcommand => subcommand + .setName('leaderboard') + .setDescription('See the current leaderboard') + ); + }, { guildIds: ['700378785605877820'] }); + } + + private async newMeetup(id: string, date: Date, author: User, attendees: User[]): Promise { + const confirmedMembers = [author.id]; + const nonUniquePossibleMembers = [author.id, ...attendees.map(user => user.id)]; + const possibleMembers = [...new Set(nonUniquePossibleMembers)]; await this.container.db.meetups.insertOne({ - id: resp.id, + id, confirmedMembers, possibleMembers, date }); - resp.edit({ + return { content: `<@${possibleMembers.join('>, <@')}>, did you all meet up on ${time(date, 'D')}?`, components: [ new ActionRowBuilder().addComponents( new ButtonBuilder() .setLabel('I was there!') - .setCustomId(`meetup|${resp.id}`) + .setCustomId(`meetup|${id}`) .setStyle(ButtonStyle.Success) ) ] - }); + }; + } + + public async messageNewMeetup(msg: Message, args: Args) { + const resp = await sendLoadingMessage(msg); + const date = await args.pick('date'); + const attendees = [...await args.repeat('user')]; + resp.edit(await this.newMeetup(resp.id, date, msg.author, attendees)); + } + + public async chatInputNewMeetup(interaction: Command.ChatInputCommandInteraction) { + await interaction.deferReply(); + const date = new Date(interaction.options.getString('date', true)); + const attendees: User[] = []; + + for (let i = 1; i <= 10; i++) { + const maybeAttendee = interaction.options.getUser(`attendee${i}`); + if (maybeAttendee) { + attendees.push(maybeAttendee); + } + } + + interaction.editReply(await this.newMeetup(interaction.id, date, interaction.user, attendees)); + } + + public async messageLeaderboard(msg: Message) { + const resp = await sendLoadingMessage(msg); + if (!msg.inGuild()) { + throw new UserError({ message: 'This command must be run in a server.', identifier: 'NoGuildLeaderboardRun' }); + } + resp.edit(await this.createLeaderboard(msg.guild)); + } + + public async chatInputLeaderboard(interaction: Command.ChatInputCommandInteraction) { + await interaction.deferReply(); + if (!interaction.guild) { + throw new UserError({ message: 'This command must be run in a server.', identifier: 'NoGuildLeaderboardRun' }); + } + interaction.editReply(await this.createLeaderboard(interaction.guild)); + } + + private async createLeaderboard(committiee: Guild): Promise { + const stats = await this.container.db.meetups.aggregate([ + { + $unwind: '$confirmedMembers' + }, + { + $group: { + _id: '$confirmedMembers', + count: { $sum: 1 }, + latestDate: { $max: '$date' } + } + }, + { + $sort: { count: -1 } + } + ]).toArray(); + + const members = await Promise.all(stats.map(async (stat) => { + const member = await committiee.members.fetch(stat._id); + return { + name: member.displayName, + pfp: await loadImage(member.displayAvatarURL({ extension: 'png' })), + count: stat.count, + latestDate: stat.latestDate + }; + })); + + const background = await loadImage(readFileSync(`${__dirname}../../../../src/assets/committieeLeaderboardBackground.svg`)); + const canvas = createCanvas(background.width, background.height); + const ctx = canvas.getContext('2d'); + ctx.font = '35px Sans'; + ctx.fillStyle = '#FFFFFF'; + + ctx.drawImage(background, 0, 0); + + const podiumPositions = [ + { x: 330, y: 20 }, + { x: 152, y: 80 }, + { x: 508, y: 158 } + ] as const; + + for (let i = 0; i < members.length; i++) { + if (i < 3) { + ctx.resetTransform(); + ctx.save(); + ctx.translate(podiumPositions[i].x, podiumPositions[i].y); + ctx.beginPath(); + ctx.arc(45, 45, 45, 0, Math.PI * 2); + ctx.clip(); + ctx.drawImage(members[i].pfp, 0, 0, 90, 90); + ctx.restore(); + } + + ctx.resetTransform(); + ctx.translate(30, 375 + (i * 45)); + ctx.save(); + ctx.beginPath(); + ctx.arc(35 / 2, 35 / 2, 35 / 2, 0, Math.PI * 2); + ctx.clip(); + ctx.drawImage(members[i].pfp, 0, 0, 35, 35); + ctx.restore(); + ctx.fillText(`${members[i].count}|${members[i].name}`, 45, 25); + } + + return { + content: '', + embeds: [ + new EmbedBuilder() + .setImage('attachment://background.png') + ], + files: [ + { attachment: canvas.toBuffer('image/png'), name: 'background.png' } + ] + }; } } + +type LeaderboardStat = { + _id: string; + count: number; + latestDate: Date; +}; + +// Useful for leaderboard +// db.meetups.aggregate([ +// { +// $unwind: "$confirmedMembers" +// }, +// { +// $group: { +// _id: "$confirmedMembers", +// count: { $sum: 1 }, +// latestDate: { $max: "$date" } +// } +// }, +// { +// $sort: { count: -1 } +// } +// ]); diff --git a/src/interaction-handlers/committieeMeetup.ts b/src/interaction-handlers/committieeMeetup.ts new file mode 100644 index 0000000..c2986d3 --- /dev/null +++ b/src/interaction-handlers/committieeMeetup.ts @@ -0,0 +1,51 @@ +import { ApplyOptions } from '@sapphire/decorators'; +import { InteractionHandlerOptions, InteractionHandlerTypes, InteractionHandler } from '@sapphire/framework'; +import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle } from 'discord.js'; + +@ApplyOptions({ + interactionHandlerType: InteractionHandlerTypes.Button +}) +export class CommittieeMeetup extends InteractionHandler { + + public async parse(interaction: ButtonInteraction) { + if (!interaction.customId.startsWith('meetup')) { + return this.none(); + } + await interaction.deferReply({ ephemeral: true }); + return this.some(); + } + + + public async run(interaction: ButtonInteraction) { + const meetup = await this.container.db.meetups.findOne({ id: interaction.customId.split('|')[1] }); + + if (!meetup) { + return interaction.editReply('Something went wrong and I couldn\'t find this meetup :('); + } + + if (!meetup.possibleMembers.includes(interaction.user.id)) { + return interaction.editReply('Looks like you aren\'t a part of this meetup'); + } + + if (meetup.confirmedMembers.includes(interaction.user.id)) { + return interaction.editReply('You\'re already confirmed'); + } + + meetup.confirmedMembers.push(interaction.user.id); + await this.container.db.meetups.updateOne({ id: meetup.id }, { $set: { confirmedMembers: meetup.confirmedMembers } }); + + if (meetup.possibleMembers.length >= meetup.confirmedMembers.length) { + await interaction.message.edit({ components: [ + new ActionRowBuilder().setComponents(new ButtonBuilder() + .setLabel('Everyone confirmed!') + .setCustomId(`meetup|${meetup.id}`) + .setStyle(ButtonStyle.Primary) + .setDisabled(true) + ) + ] }); + } + + return interaction.editReply('Thanks for confirming!'); + } + +}