Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
BenSegal855 committed Nov 5, 2024
2 parents 0edfc8d + 480e2e1 commit cf4ab7a
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 5 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -62,4 +62,4 @@ jobs:
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
21 changes: 21 additions & 0 deletions src/assets/committieeLeaderboardBackground.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
224 changes: 224 additions & 0 deletions src/commands/Info/committiee.ts
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 }
// }
// ]);
51 changes: 51 additions & 0 deletions src/interaction-handlers/committieeMeetup.ts
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!');
}

}
7 changes: 5 additions & 2 deletions src/lib/mongo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import type {
ChannelRename,
QuickRoll,
RPCharter,
RollImportCharacter
RollImportCharacter,
Meetup
} from '@lib/types/database';

export interface SteveCollections {
Expand All @@ -24,6 +25,7 @@ export interface SteveCollections {
quickRolls: Collection<QuickRoll>;
rpCharacters: Collection<RPCharter>;
rollImportCharacters: Collection<RollImportCharacter>;
meetups: Collection<Meetup>
}

export async function startMongo() {
Expand All @@ -46,7 +48,8 @@ export async function startMongo() {
channelRename: database.collection<ChannelRename>('channelRename'),
quickRolls: database.collection<QuickRoll>('quickRolls'),
rpCharacters: database.collection<RPCharter>('rpCharacters'),
rollImportCharacters: database.collection<RollImportCharacter>('rollImportCharacters')
rollImportCharacters: database.collection<RollImportCharacter>('rollImportCharacters'),
meetups: database.collection<Meetup>('meetups')
};

container.mongo = mongo;
Expand Down
1 change: 1 addition & 0 deletions src/lib/types/database/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './quickRoll';
export * from './idHint';
export * from './rpCharacter';
export * from './rollImportCharacter';
export * from './meetup';
6 changes: 6 additions & 0 deletions src/lib/types/database/meetup.d.ts
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;
};

0 comments on commit cf4ab7a

Please sign in to comment.