-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
311 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
import { ApplyOptions } from '@sapphire/decorators'; | ||
import { UserError, type Args, type Command } from '@sapphire/framework'; | ||
import { SubcommandOptions } from '@sapphire/plugin-subcommands'; | ||
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<SubcommandOptions>({ | ||
description: 'Find out and log Committee meetups', | ||
preconditions: ['CommitteeOnly'], | ||
detailedDescription: { | ||
usage: 'meetup <Date in Mon-DD-YYYY> <list of attendees>\nleaderboard', | ||
examples: [ | ||
'meetup Apr-28-2023 <@696783853267976202>', | ||
'leaderboard' | ||
] | ||
}, | ||
subcommands: [ | ||
{ | ||
name: 'meetup', | ||
messageRun: 'messageNewMeetup', | ||
chatInputRun: 'chatInputNewMeetup' | ||
}, | ||
{ | ||
name: 'leaderboard', | ||
messageRun: 'messageLeaderboard', | ||
chatInputRun: 'chatInputLeaderboard' | ||
} | ||
] | ||
}) | ||
export class UserCommand extends SteveSubcommand { | ||
|
||
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) | ||
); | ||
|
||
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<BaseMessageOptions> { | ||
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, | ||
confirmedMembers, | ||
possibleMembers, | ||
date | ||
}); | ||
|
||
return { | ||
content: `<@${possibleMembers.join('>, <@')}>, did you all meet up on ${time(date, 'D')}?`, | ||
components: [ | ||
new ActionRowBuilder<ButtonBuilder>().addComponents( | ||
new ButtonBuilder() | ||
.setLabel('I was there!') | ||
.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<BaseMessageOptions> { | ||
const stats = await this.container.db.meetups.aggregate<LeaderboardStat>([ | ||
{ | ||
$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 } | ||
// } | ||
// ]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<InteractionHandlerOptions>({ | ||
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<ButtonBuilder>().setComponents(new ButtonBuilder() | ||
.setLabel('Everyone confirmed!') | ||
.setCustomId(`meetup|${meetup.id}`) | ||
.setStyle(ButtonStyle.Primary) | ||
.setDisabled(true) | ||
) | ||
] }); | ||
} | ||
|
||
return interaction.editReply('Thanks for confirming!'); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export type Meetup = { | ||
id: string; | ||
possibleMembers: string[]; | ||
confirmedMembers: string[] | ||
date: Date; | ||
}; |