Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,20 @@ npm start
## Available Commands

- `/ping` - Bot responds with "Pong!"

## Admin commands

- `/adminrole` - Set an administrator role to use admin commands
- `/approvechannel` - Set the channel for submissions to be forwarded to for approval
- `/create-event` `/edit-event` - Create/Edit an event
- `/create-team` `/edit-team` - Create/Edit a team
- `/register-team` - Register a team for an event

## Public commands

- `/event-info` - Information for an event and teams
- `/team-info` - Information for a team and members

## Event commands (for participants)

- `/submit` - Submit an image to the event
19 changes: 19 additions & 0 deletions prisma/migrations/20250415182835_active_event/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Event" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"description" TEXT,
"startDate" DATETIME NOT NULL,
"endDate" DATETIME,
"status" TEXT NOT NULL DEFAULT 'PLANNED',
"active" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Event" ("createdAt", "description", "endDate", "id", "name", "startDate", "status", "updatedAt") SELECT "createdAt", "description", "endDate", "id", "name", "startDate", "status", "updatedAt" FROM "Event";
DROP TABLE "Event";
ALTER TABLE "new_Event" RENAME TO "Event";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ model Event {
startDate DateTime
endDate DateTime?
status String @default("PLANNED") // PLANNED, ONGOING, COMPLETED, CANCELLED
active Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

Expand Down
39 changes: 39 additions & 0 deletions src/commands/admin/event/activateevent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
import { EventService } from '#services/eventService.js';
import logger from '#utils/logger.js';

export const data = new SlashCommandBuilder()
.setName('activate-event')
.setDescription('Set the event to active for the bot context')
.addStringOption((option) =>
option
.setName('event')
.setDescription('The event to activate')
.setRequired(true)
.setAutocomplete(true)
);

/**
* @param {ChatInputCommandInteraction} interaction
*/
export async function execute(interaction) {
const eventId = interaction.options.getString('event');
try {
const event = await EventService.activateEvent(eventId);
await interaction.reply(`Event ${event.name} has been activated.`);
} catch (error) {
logger.error('Error activating event', error);
await interaction.reply('An error occurred while activating the event.');
}
}

/**
* @param {ChatInputCommandInteraction} interaction
*/
export async function autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
const events = await EventService.getEvents();
const filtered = events.filter((event) => event.name.startsWith(focusedValue));
const choices = filtered.map((event) => event).slice(0, 25);
await interaction.respond(choices.map((choice) => ({ name: choice.name, value: choice.id })));
}
52 changes: 52 additions & 0 deletions src/commands/admin/team/registerteam.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
import { EventService } from '#services/eventService.js';
import { TeamService } from '#services/teamService.js';
import logger from '#utils/logger.js';

export const data = new SlashCommandBuilder()
.setName('register-team')
.setDescription('Register team to the event')
.addStringOption((option) =>
option
.setName('team')
.setDescription('Select the team to register')
.setRequired(true)
.setAutocomplete(true)
);

/**
* @param {ChatInputCommandInteraction} interaction
*/
export async function execute(interaction) {
const teamId = interaction.options.getString('team');
// Register the team to the event
const event = await EventService.getActiveEvent();
if (!event) {
await interaction.reply('No active event found');
return;
}
const team = await TeamService.getTeamById(teamId);
if (!team) {
await interaction.reply('Team not found');
return;
}
try {
await EventService.registerTeamToEvent(event.id, teamId);
await interaction.reply(`Team ${team.name} registered to event ${event.name}`);
logger.info(`Team ${teamId} registered to event ${event.id}`);
} catch (error) {
logger.error('Error registering team to event', error);
await interaction.reply('An error occurred while registering the team to the event.');
}
}

/**
* @param {ChatInputCommandInteraction} interaction
*/
export async function autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
const teams = await TeamService.getTeams();
const filtered = teams.filter((team) => team.name.startsWith(focusedValue));
const choices = filtered.map((event) => event).slice(0, 25);
await interaction.respond(choices.map((choice) => ({ name: choice.name, value: choice.id })));
}
55 changes: 55 additions & 0 deletions src/commands/event/eventinfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
import { EventService } from '#services/eventService.js';
import logger from '#utils/logger.js';

export const data = new SlashCommandBuilder()
.setName('event-info')
.setDescription('Get Event information')
.addStringOption((option) =>
option
.setName('event')
.setDescription('Select the event to get information about')
.setRequired(false)
.setAutocomplete(true)
);

/**
* @param {ChatInputCommandInteraction} interaction
*/
export async function execute(interaction) {
const eventId = interaction.options.getString('event');
const event = eventId
? await EventService.getEventById(eventId)
: await EventService.getActiveEvent();
if (!event) {
await interaction.reply('Event not found');
return;
}
const embed = {
title: event.name,
description: event.description,
fields: [
{ name: 'Start Date', value: event.startDate.toString(), inline: true },
{ name: 'End Date', value: event.endDate.toString(), inline: true },
{ name: 'Status', value: event.status, inline: true },
{
name: 'Teams',
value: event.teams.map((team) => team.name).join(', ') || 'None',
inline: false,
},
],
};
await interaction.reply({ embeds: [embed] });
logger.info(`Event info requested: ${event.name}`);
}

/**
* @param {ChatInputCommandInteraction} interaction
*/
export async function autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
const events = await EventService.getEvents();
const filtered = events.filter((event) => event.name.startsWith(focusedValue));
const choices = filtered.map((event) => event).slice(0, 25);
await interaction.respond(choices.map((choice) => ({ name: choice.name, value: choice.id })));
}
44 changes: 44 additions & 0 deletions src/commands/event/teaminfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js';
import { TeamService } from '#services/teamService.js';
import logger from '#utils/logger.js';

export const data = new SlashCommandBuilder()
.setName('team-info')
.setDescription('Get Team information')
.addStringOption((option) =>
option
.setName('team')
.setDescription('Select the team to get information about')
.setRequired(true)
.setAutocomplete(true)
);

/**
* @param {ChatInputCommandInteraction} interaction
*/
export async function execute(interaction) {
const teamId = interaction.options.getString('team');
const team = await TeamService.getTeamById(teamId);
if (!team) {
await interaction.reply('Team not found');
return;
}
const embed = {
title: team.name,
description: team.description,
fields: [{ name: 'Leader', value: `<@${team.leader.discordId}>`, inline: true }],
};
await interaction.reply({ embeds: [embed] });
logger.info(`Team info requested: ${team.name}`);
}

/**
* @param {ChatInputCommandInteraction} interaction
*/
export async function autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
const events = await TeamService.getTeams();
const filtered = events.filter((event) => event.name.startsWith(focusedValue));
const choices = filtered.map((event) => event).slice(0, 25);
await interaction.respond(choices.map((choice) => ({ name: choice.name, value: choice.id })));
}
97 changes: 97 additions & 0 deletions src/services/eventService.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,31 @@ export class EventService {
}
}

/**
* Get an event by ID
* @param {String} eventId - The ID of the event
* @return {Promise<import('@prisma/client').Event>} The event object
*/
static async getEventById(eventId) {
try {
const event = await prisma.event.findUnique({
where: {
id: eventId,
},
include: {
teams: true,
},
});
if (!event) {
throw new Error(`Event with ID ${eventId} not found`);
}
return event;
} catch (error) {
logger.error(`Error getting event with ID ${eventId}:`, error);
throw error;
}
}

/**
* Update an event by ID
* @param {String} eventId - The ID of the event
Expand Down Expand Up @@ -95,4 +120,76 @@ export class EventService {
throw error;
}
}

/**
* Activate event
*/
static async activateEvent(eventId) {
try {
// Deactivate all other events
await prisma.event.updateMany({
where: {
active: true,
},
data: {
active: false,
},
});
return await prisma.event.update({
where: {
id: eventId,
},
data: {
active: true,
},
});
} catch (error) {
logger.error(`Error activating event with ID ${eventId}:`, error);
throw error;
}
}

/**
* Regsiter team to event
*/
static async registerTeamToEvent(eventId, teamId) {
try {
await prisma.event.update({
where: {
id: eventId,
},
data: {
teams: {
connect: {
id: teamId,
},
},
},
});
} catch (error) {
console.log(error);
logger.error(`Error registering team ${teamId} to event ${eventId}:`, error);
throw error;
}
}

/**
* Get active event
*/
static async getActiveEvent() {
try {
const event = await prisma.event.findFirst({
where: {
active: true,
},
include: {
teams: true,
},
});
return event;
} catch (error) {
logger.error('Error getting active event', error);
throw error;
}
}
}
7 changes: 6 additions & 1 deletion src/services/teamService.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,18 @@ export class TeamService {
/**
* Get a team by ID
* @param {String} teamId - The ID of the team
* @returns {Promise<import('@prisma/client').Team>} The team object
* @returns {Promise<import('@prisma/client').Team & { leader: import('@prisma/client').DiscordUser }>} The team object
*/
static async getTeamById(teamId) {
try {
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
include: {
leader: true,
members: true,
},
});
if (!team) {
throw new Error('Team not found');
Expand All @@ -90,6 +94,7 @@ export class TeamService {
throw error;
}
}

/**
* Update a team by ID
* @param {String} teamId - The ID of the team
Expand Down