diff --git a/README.md b/README.md index 436e70d..4c62967 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,40 @@ npm start ## Available Commands - `/ping` - Bot responds with "Pong!" + +## Admin commands + +- `/approvechannel` - Set the channel for submissions to be forwarded to for approval +- `/signupchannel` - Set the channel to announce registered members +- `/acceptedchannel` - Set the channel to announced approved submissions + +## Mod commands + +- `/create-event` `/edit-event` - Create/Edit an event +- `/create-team` `/edit-team` - Create/Edit a team +- `/activate-event` - Activate the event for team commands +- `/register-team` - Register a team for an event + +## Public commands + +- `/event` - Information for an event and teams +- `/team` - Information for a team and members +- `/register` - Register yourself to the event +- `/status` - Status for yourself + +## Event commands (for participants) + +- `/submit` - Submit an image to the event + +## Usage + +1. Setting up event and team + 1. /create-event name: CAKE description: yumyum + 2. /activate-event event: CAKE + 3. /create-team leader: @ruto name: BESTTEAM + - Note: They must already be in discord + 4. /register-team team: CAKE +2. Other users must now register + 1. /register rsn: noah +3. Mod can now add user to a team + 1. /addmember team: CAKE user: @Noah diff --git a/package.json b/package.json index fe4627a..8a4e33b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", "prisma:studio": "prisma studio", - "prisma:seed": "node prisma/seed.js" + "prisma:seed": "node prisma/seed.js", + "commands": "node ./deploy_commands.js" }, "dependencies": { "@prisma/client": "^5.10.0", diff --git a/prisma/migrations/20250415182835_active_event/migration.sql b/prisma/migrations/20250415182835_active_event/migration.sql new file mode 100644 index 0000000..dbd4ac3 --- /dev/null +++ b/prisma/migrations/20250415182835_active_event/migration.sql @@ -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; diff --git a/prisma/migrations/20250415212015_add_rsn_to_participant/migration.sql b/prisma/migrations/20250415212015_add_rsn_to_participant/migration.sql new file mode 100644 index 0000000..9325eb4 --- /dev/null +++ b/prisma/migrations/20250415212015_add_rsn_to_participant/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventParticipant" ADD COLUMN "rsn" TEXT; diff --git a/prisma/migrations/20250416003354_participant_note/migration.sql b/prisma/migrations/20250416003354_participant_note/migration.sql new file mode 100644 index 0000000..5811af6 --- /dev/null +++ b/prisma/migrations/20250416003354_participant_note/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventParticipant" ADD COLUMN "note" TEXT; diff --git a/prisma/migrations/20250416004724_remove_uuid_for_participant/migration.sql b/prisma/migrations/20250416004724_remove_uuid_for_participant/migration.sql new file mode 100644 index 0000000..7499c71 --- /dev/null +++ b/prisma/migrations/20250416004724_remove_uuid_for_participant/migration.sql @@ -0,0 +1,26 @@ +/* + Warnings: + + - The primary key for the `EventParticipant` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `id` on the `EventParticipant` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_EventParticipant" ( + "userId" TEXT NOT NULL, + "eventId" TEXT NOT NULL, + "rsn" TEXT, + "note" TEXT, + "status" TEXT NOT NULL DEFAULT 'REGISTERED', + + PRIMARY KEY ("userId", "eventId"), + CONSTRAINT "EventParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "DiscordUser" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "EventParticipant_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_EventParticipant" ("eventId", "note", "rsn", "status", "userId") SELECT "eventId", "note", "rsn", "status", "userId" FROM "EventParticipant"; +DROP TABLE "EventParticipant"; +ALTER TABLE "new_EventParticipant" RENAME TO "EventParticipant"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/migrations/20250416005059_fix_participant_relation/migration.sql b/prisma/migrations/20250416005059_fix_participant_relation/migration.sql new file mode 100644 index 0000000..a22be8b --- /dev/null +++ b/prisma/migrations/20250416005059_fix_participant_relation/migration.sql @@ -0,0 +1,19 @@ +-- RedefineTables +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_EventParticipant" ( + "userId" TEXT NOT NULL, + "eventId" TEXT NOT NULL, + "rsn" TEXT, + "note" TEXT, + "status" TEXT NOT NULL DEFAULT 'REGISTERED', + + PRIMARY KEY ("userId", "eventId"), + CONSTRAINT "EventParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "DiscordUser" ("discordId") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "EventParticipant_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_EventParticipant" ("eventId", "note", "rsn", "status", "userId") SELECT "eventId", "note", "rsn", "status", "userId" FROM "EventParticipant"; +DROP TABLE "EventParticipant"; +ALTER TABLE "new_EventParticipant" RENAME TO "EventParticipant"; +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6182403..aa3b2d1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 @@ -71,14 +72,15 @@ model TeamMember { } model EventParticipant { - id String @id @default(uuid()) userId String eventId String + rsn String? // Registered in-game name + note String? status String @default("REGISTERED") // REGISTERED, INTERESTED, UNPAID - user DiscordUser @relation(fields: [userId], references: [id]) + user DiscordUser @relation(fields: [userId], references: [discordId]) event Event @relation(fields: [eventId], references: [id]) - @@unique([userId, eventId]) + @@id([userId, eventId]) } model Submission { diff --git a/src/commands/admin/acceptedchannel.js b/src/commands/admin/acceptedchannel.js new file mode 100644 index 0000000..5ff8443 --- /dev/null +++ b/src/commands/admin/acceptedchannel.js @@ -0,0 +1,43 @@ +import { + ChannelType, + ChatInputCommandInteraction, + PermissionFlagsBits, + SlashCommandBuilder, +} from 'discord.js'; +import { ConfigService } from '#services/configService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('acceptedchannel') + .setDescription('Get or set the accepted submission channel') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addChannelOption((option) => + option + .setName('channel') + .setDescription('Channel to forward accepted submissions to') + .setRequired(false) + .addChannelTypes(ChannelType.GuildText) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const channel = interaction.options.getChannel('channel'); + + // if channel doesn't exist, get current channel if it exists + if (!channel) { + const currentChannel = await ConfigService.getAcceptedChannel(interaction.guildId); + if (currentChannel) { + await interaction.reply(`Current accepted channel: <#${currentChannel}>`); + } else { + await interaction.reply('No accepted channel set.'); + } + return; + } + + // if channel exists, set it as the accepted channel + await ConfigService.setAcceptedChannel(interaction.guildId, channel.id); + await interaction.reply(`Accepted channel set to <#${channel.id}>`); + logger.info(`Accepted channel set to ${channel.id} for guild ${interaction.guildId}`); +} diff --git a/src/commands/admin/adminrole.js b/src/commands/admin/adminrole.js deleted file mode 100644 index 6ea8229..0000000 --- a/src/commands/admin/adminrole.js +++ /dev/null @@ -1,34 +0,0 @@ -import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; -import { ConfigService } from '#services/configService.js'; -import logger from '#utils/logger.js'; - -export const data = new SlashCommandBuilder() - .setName('adminrole') - .setDescription('Get or set admin role') - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) - .addRoleOption((option) => - option.setName('role').setDescription('Role to be set as admin role').setRequired(false) - ); - -/** - * @param {ChatInputCommandInteraction} interaction - */ -export async function execute(interaction) { - const role = interaction.options.getRole('role'); - - // if role doesn't exist, get role channel if it exists - if (!role) { - const currentRole = await ConfigService.getAdminRole(interaction.guildId); - if (currentRole) { - await interaction.reply(`Current admin role: <@&${currentRole}>`); - } else { - await interaction.reply('No admin role set.'); - } - return; - } - - // if role exists, set it as the admin role - await ConfigService.setAdminRole(interaction.guildId, role.id); - await interaction.reply(`Admin role set to <@&${role.id}>`); - logger.info(`Admin role set to ${role.id} for guild ${interaction.guildId}`); -} diff --git a/src/commands/admin/approvechannel.js b/src/commands/admin/approvechannel.js index 4986270..aa2ff2c 100644 --- a/src/commands/admin/approvechannel.js +++ b/src/commands/admin/approvechannel.js @@ -1,10 +1,16 @@ -import { ChannelType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { + ChannelType, + ChatInputCommandInteraction, + PermissionFlagsBits, + SlashCommandBuilder, +} from 'discord.js'; import { ConfigService } from '#services/configService.js'; import logger from '#utils/logger.js'; export const data = new SlashCommandBuilder() .setName('approvechannel') .setDescription('Get or set the approval submission channel') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .addChannelOption((option) => option .setName('channel') @@ -30,22 +36,6 @@ export async function execute(interaction) { return; } - // Check if user has admin permissions - // Check custom role first - if (!interaction.member.permissions.has('Administrator')) { - const customRole = await ConfigService.getAdminRole(interaction.guildId); - if (customRole) { - const member = await interaction.guild.members.fetch(interaction.user.id); - if (!member.roles.cache.has(customRole)) { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } else { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } - // if channel exists, set it as the approval channel await ConfigService.setApprovalChannel(interaction.guildId, channel.id); await interaction.reply(`Approval channel set to <#${channel.id}>`); diff --git a/src/commands/admin/event/activateevent.js b/src/commands/admin/event/activateevent.js new file mode 100644 index 0000000..6e3b3bd --- /dev/null +++ b/src/commands/admin/event/activateevent.js @@ -0,0 +1,40 @@ +import { ChatInputCommandInteraction, PermissionFlagsBits, 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') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .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 }))); +} diff --git a/src/commands/admin/event/createevent.js b/src/commands/admin/event/createevent.js index 2887416..35c5c4e 100644 --- a/src/commands/admin/event/createevent.js +++ b/src/commands/admin/event/createevent.js @@ -1,11 +1,11 @@ -import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; -import { ConfigService } from '#services/configService.js'; +import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; import logger from '#utils/logger.js'; import { EventService } from '#services/eventService.js'; export const data = new SlashCommandBuilder() .setName('create-event') .setDescription('Create an event') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .addStringOption((option) => option.setName('name').setDescription('The name of the event').setRequired(true) ) @@ -23,20 +23,6 @@ export const data = new SlashCommandBuilder() * @param {ChatInputCommandInteraction} interaction */ export async function execute(interaction) { - if (!interaction.member.permissions.has('Administrator')) { - const customRole = await ConfigService.getAdminRole(interaction.guildId); - if (customRole) { - const member = await interaction.guild.members.fetch(interaction.user.id); - if (!member.roles.cache.has(customRole)) { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } else { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } - const name = interaction.options.getString('name'); const description = interaction.options.getString('description'); const startDate = interaction.options.getString('startdate'); diff --git a/src/commands/admin/event/editevent.js b/src/commands/admin/event/editevent.js index 14f344b..8f99ee5 100644 --- a/src/commands/admin/event/editevent.js +++ b/src/commands/admin/event/editevent.js @@ -1,11 +1,11 @@ -import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; -import { ConfigService } from '#services/configService.js'; +import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; import logger from '#utils/logger.js'; import { EventService } from '#services/eventService.js'; export const data = new SlashCommandBuilder() .setName('edit-event') .setDescription('Edit an event') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .addStringOption((option) => option .setName('event') @@ -42,20 +42,6 @@ export const data = new SlashCommandBuilder() * @param {ChatInputCommandInteraction} interaction */ export async function execute(interaction) { - if (!interaction.member.permissions.has('Administrator')) { - const customRole = await ConfigService.getAdminRole(interaction.guildId); - if (customRole) { - const member = await interaction.guild.members.fetch(interaction.user.id); - if (!member.roles.cache.has(customRole)) { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } else { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } - const eventId = interaction.options.getString('event'); const name = interaction.options.getString('name'); const description = interaction.options.getString('description'); diff --git a/src/commands/admin/signupchannel.js b/src/commands/admin/signupchannel.js new file mode 100644 index 0000000..a95153f --- /dev/null +++ b/src/commands/admin/signupchannel.js @@ -0,0 +1,43 @@ +import { + ChannelType, + ChatInputCommandInteraction, + PermissionFlagsBits, + SlashCommandBuilder, +} from 'discord.js'; +import { ConfigService } from '#services/configService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('signupchannel') + .setDescription('Get or set the signed up channel') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addChannelOption((option) => + option + .setName('channel') + .setDescription('Channel to forward signups to') + .setRequired(false) + .addChannelTypes(ChannelType.GuildText) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const channel = interaction.options.getChannel('channel'); + + // if channel doesn't exist, get current channel if it exists + if (!channel) { + const currentChannel = await ConfigService.getSignedUpChannel(interaction.guildId); + if (currentChannel) { + await interaction.reply(`Current signed up channel: <#${currentChannel}>`); + } else { + await interaction.reply('No signed up channel set.'); + } + return; + } + + // if channel exists, set it as the signed up channel + await ConfigService.setSignedUpChannel(interaction.guildId, channel.id); + await interaction.reply(`Signed up channel set to <#${channel.id}>`); + logger.info(`Signed up channel set to ${channel.id} for guild ${interaction.guildId}`); +} diff --git a/src/commands/admin/team/addmember.js b/src/commands/admin/team/addmember.js new file mode 100644 index 0000000..6e69bea --- /dev/null +++ b/src/commands/admin/team/addmember.js @@ -0,0 +1,60 @@ +import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; +import { TeamService } from '#services/teamService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('addmember') + .setDescription('Add a registered member to a team.') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) + .addStringOption((option) => + option + .setName('team') + .setDescription('The team to add the member to.') + .setRequired(true) + .setAutocomplete(true) + ) + .addUserOption((option) => + option.setName('member').setDescription('The member to add to the team.').setRequired(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const teamId = interaction.options.getString('team'); + const member = interaction.options.getUser('member'); + + // Check if the user is already in the team + const team = await TeamService.getTeamById(teamId); + const isMember = team.members.some((member) => member.user.discordId === member.id); + if (isMember) { + await interaction.reply('This user is already a member of the team.'); + return; + } + // Check if the user is already in another team + if (await TeamService.isUserInTeam(member.id)) { + await interaction.reply('This user is already in another team.'); + return; + } + try { + const team = await TeamService.addMemberToTeam(teamId, member.id); + await interaction.reply(`Member <@${member.id}> added to team ${team.name} successfully!`); + logger.info( + `Member ${member.username} - ${member.id} added to team ${team.name} ${team.id} successfully!` + ); + } catch (error) { + logger.error('Error adding member to team', error); + await interaction.reply('An error occurred while adding the member to the team.'); + } +} + +/** + * @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 }))); +} diff --git a/src/commands/admin/team/createteam.js b/src/commands/admin/team/createteam.js index 01457c7..57517d0 100644 --- a/src/commands/admin/team/createteam.js +++ b/src/commands/admin/team/createteam.js @@ -1,11 +1,11 @@ -import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; import { TeamService } from '#services/teamService.js'; -import { ConfigService } from '#services/configService.js'; import logger from '#utils/logger.js'; export const data = new SlashCommandBuilder() .setName('create-team') .setDescription('Create a team') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) .addUserOption((option) => option.setName('leader').setDescription('The team leader').setRequired(true) ) @@ -20,25 +20,17 @@ export const data = new SlashCommandBuilder() * @param {ChatInputCommandInteraction} interaction */ export async function execute(interaction) { - if (!interaction.member.permissions.has('Administrator')) { - const customRole = await ConfigService.getAdminRole(interaction.guildId); - if (customRole) { - const member = await interaction.guild.members.fetch(interaction.user.id); - if (!member.roles.cache.has(customRole)) { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } else { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } const leader = interaction.options.getUser('leader'); const name = interaction.options.getString('name'); const description = interaction.options.getString('description'); try { - await TeamService.createTeam(leader.id, name, description); + if (TeamService.isUserInTeam(leader.id)) { + await interaction.reply(`User <@${leader.id}> is already in a team.`); + return; + } + const team = await TeamService.createTeam(leader.id, name, description); + await TeamService.addMemberToTeam(team.id, leader.id); // Add the leader to their own team await interaction.reply(`Team "${name}" (Leader: ${leader.tag}) created successfully!`); logger.info(`Team "${name}" created successfully by ${interaction.user.tag}`); } catch (error) { diff --git a/src/commands/admin/team/editteam.js b/src/commands/admin/team/editteam.js index c956d01..c6002ac 100644 --- a/src/commands/admin/team/editteam.js +++ b/src/commands/admin/team/editteam.js @@ -1,11 +1,11 @@ -import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; -import { ConfigService } from '#services/configService.js'; +import { ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; import { TeamService } from '#services/teamService.js'; import logger from '#utils/logger.js'; export const data = new SlashCommandBuilder() .setName('edit-team') .setDescription('Edit a team') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) .addStringOption((option) => option .setName('team') @@ -27,20 +27,6 @@ export const data = new SlashCommandBuilder() * @param {ChatInputCommandInteraction} interaction */ export async function execute(interaction) { - if (!interaction.member.permissions.has('Administrator')) { - const customRole = await ConfigService.getAdminRole(interaction.guildId); - if (customRole) { - const member = await interaction.guild.members.fetch(interaction.user.id); - if (!member.roles.cache.has(customRole)) { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } else { - await interaction.reply('You do not have permission to set the approval channel.'); - return; - } - } - const teamId = interaction.options.getString('team'); const leaderId = interaction.options.getUser('leader').id; const name = interaction.options.getString('name'); diff --git a/src/commands/admin/team/registerteam.js b/src/commands/admin/team/registerteam.js new file mode 100644 index 0000000..ade8281 --- /dev/null +++ b/src/commands/admin/team/registerteam.js @@ -0,0 +1,53 @@ +import { ChatInputCommandInteraction, PermissionFlagsBits, 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') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) + .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 }))); +} diff --git a/src/commands/admin/team/removemember.js b/src/commands/admin/team/removemember.js new file mode 100644 index 0000000..86cbe5d --- /dev/null +++ b/src/commands/admin/team/removemember.js @@ -0,0 +1,33 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder, PermissionFlagsBits } from 'discord.js'; +import { TeamService } from '#services/teamService.js'; +import logger from '#utils/logger.js'; + +export const data = new SlashCommandBuilder() + .setName('removemember') + .setDescription('Remove a member from the team.') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages) + .addUserOption((option) => + option.setName('member').setDescription('The member to remove from the team.').setRequired(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const member = interaction.options.getUser('member'); + + try { + // Check if the user is in a team + if (!(await TeamService.isUserInTeam(member.id))) { + await interaction.reply('This user is not in any team.'); + return; + } + // Remove member from all teams + await TeamService.removeMemberFromAllTeams(member.id); + await interaction.reply(`Member <@${member.id}> removed from the team successfully!`); + logger.info(`Member ${member.username} - ${member.id} removed from the team successfully!`); + } catch (error) { + logger.error('Error removing member from team', error); + await interaction.reply('An error occurred while removing the member from the team.'); + } +} diff --git a/src/commands/event/event.js b/src/commands/event/event.js new file mode 100644 index 0000000..c5c3e36 --- /dev/null +++ b/src/commands/event/event.js @@ -0,0 +1,57 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { EventService } from '#services/eventService.js'; +import logger from '#utils/logger.js'; +import { getColorFromName } from '#utils/color.js'; + +export const data = new SlashCommandBuilder() + .setName('event') + .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('No event found.'); + return; + } + const embed = { + title: event.name, + description: event.description, + fields: [ + { name: 'Start Date', value: event.startDate?.toString() || 'None', inline: true }, + { name: 'End Date', value: event.endDate?.toString() || 'None', inline: true }, + { name: 'Status', value: event.status, inline: true }, + { + name: 'Teams', + value: event.teams.map((team, index) => `${index + 1}. ${team.name}`).join('\n') || 'None', + inline: false, + }, + ], + color: getColorFromName(event.name), + }; + 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 }))); +} diff --git a/src/commands/event/register.js b/src/commands/event/register.js new file mode 100644 index 0000000..5352e04 --- /dev/null +++ b/src/commands/event/register.js @@ -0,0 +1,78 @@ +import { ChatInputCommandInteraction, MessageFlags, SlashCommandBuilder } from 'discord.js'; +import { EventService } from '#services/eventService.js'; +import logger from '#utils/logger.js'; +import { ConfigService } from '#services/configService.js'; + +export const data = new SlashCommandBuilder() + .setName('register') + .setDescription('Register yourself to the event') + .addStringOption((option) => + option.setName('rsn').setDescription('Your RuneScape name').setRequired(true) + ) + .addStringOption((option) => + option.setName('duo').setDescription("Your duo partner's RuneScape name").setRequired(false) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const activeEvent = await EventService.getActiveEvent(); + if (!activeEvent) { + await interaction.reply({ + content: 'There is no active event to register for.', + flags: MessageFlags.Ephemeral, + }); + return; + } + const rsn = interaction.options.getString('rsn'); + const duo = interaction.options.getString('duo'); + if (!rsn) { + await interaction.reply({ + content: 'Please provide a valid RuneScape name.', + flags: MessageFlags.Ephemeral, + }); + return; + } + try { + if (await EventService.isUserRegistered(interaction.user.id)) { + await EventService.updateUserRsn(interaction.user.id, rsn, duo); + await interaction.reply({ + content: 'You are already registered for the event. Updating details', + flags: MessageFlags.Ephemeral, + }); + logger.info( + `User ${interaction.user.username} (${interaction.user.id}) updated their RSN to ${rsn} for event ${activeEvent.name}` + ); + return; + } else { + await EventService.registerUserToEvent(interaction.user.id, rsn, duo); + await interaction.reply({ + content: + `You have successfully registered for the event with RSN: ${rsn}.` + + (duo ? ` Make sure your duo partner is registered with RSN: ${duo}.` : ''), + flags: MessageFlags.Ephemeral, + }); + logger.info( + `User ${interaction.user.username} (${interaction.user.id}) registered for event ${activeEvent.name} with RSN ${rsn}` + ); + const signedUpChannel = await ConfigService.getSignedUpChannel(interaction.guildId); + if (signedUpChannel) { + const channel = await interaction.client.channels.fetch(signedUpChannel); + if (channel && channel.isTextBased()) { + await channel.send({ + content: `<@${interaction.user.id}> has registered for the event with RSN: ${rsn}. ${duo ? `With duo partner: ${duo}` : ''}`, + allowedMentions: { parse: [] }, + }); + } + } + } + } catch (error) { + console.log(error); + logger.error('Error registering user to event', error); + await interaction.reply({ + content: 'An error occurred while registering you for the event. Please try again later.', + flags: MessageFlags.Ephemeral, + }); + } +} diff --git a/src/commands/event/status.js b/src/commands/event/status.js new file mode 100644 index 0000000..571c20f --- /dev/null +++ b/src/commands/event/status.js @@ -0,0 +1,45 @@ +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('status') + .setDescription('Your current status in the event') + .addUserOption((option) => + option.setName('user').setDescription('The user to get the status of').setRequired(false) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const user = interaction.options.getUser('user') || interaction.user; + const event = await EventService.getActiveEvent(); + if (!event) { + await interaction.reply('No active event found'); + return; + } + if (!(await EventService.isUserRegistered(user.id))) { + await interaction.reply({ + content: 'You are not registered for the event.', + }); + return; + } + const team = await TeamService.getCurrentTeam(user.id); + const userDetail = await EventService.getUserDetails(user.id); + const fields = [ + { name: 'Your RSN', value: userDetail.rsn || 'None', inline: true }, + ...(userDetail.note + ? [{ name: 'Your Duo Partner', value: userDetail.note, inline: true }] + : []), + { name: 'Team', value: team?.name || 'None', inline: true }, + ]; + const embed = { + title: event.name, + description: event.description, + fields, + }; + await interaction.reply({ embeds: [embed] }); + logger.info(`Event status requested by ${user.tag}`); +} diff --git a/src/commands/event/submit.js b/src/commands/event/submit.js index 8beaba3..4c7f7c4 100644 --- a/src/commands/event/submit.js +++ b/src/commands/event/submit.js @@ -1,6 +1,16 @@ -import { ChannelType, ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; -import logger from '#utils/logger.js'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ChannelType, + ChatInputCommandInteraction, + MessageFlags, + SlashCommandBuilder, +} from 'discord.js'; import { ConfigService } from '#services/configService.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('submit') @@ -47,6 +57,41 @@ export async function execute(interaction) { } } + // Check if the user is registered for the event' + if (!(await EventService.isUserRegistered(interaction.user.id))) { + await interaction.reply({ + content: 'You are not registered for the event. Please register first.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + // Check if user is part of a team + if (!(await TeamService.isUserInTeam(interaction.user.id))) { + await interaction.reply({ + content: 'You are not part of a team. Please contact your team leader.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + // Check if the event has started + const activeEvent = await EventService.getActiveEvent(); + if (!activeEvent) { + await interaction.reply({ + content: 'There is no active event to submit items for.', + flags: MessageFlags.Ephemeral, + }); + return; + } + if (activeEvent.status !== 'ONGOING') { + await interaction.reply({ + content: 'The event is not active. Please check back later.', + flags: MessageFlags.Ephemeral, + }); + return; + } + const name = interaction.options.getString('name'); const value = interaction.options.getString('value'); const image = interaction.options.getString('image'); @@ -59,10 +104,33 @@ export async function execute(interaction) { } logger.info(`Item submitted: ${name} - ${value}`); - await interaction.reply(`Item submitted: ${name} - ${value} - Proof type: ${proof}`); + await interaction.reply(`Item submitted: ${name} - ${value}`); // Forward the submission to the approval channel + const embed = { + title: 'New Item Submission', + description: `Name: ${name}\nValue: ${value}`, + image: { url: proof }, + fields: [{ name: 'Status', value: 'Pending', inline: false }], + footer: { text: `Submitted by: ${interaction.user.tag}` }, + }; + if (attachment) { + embed.image.url = attachment.url; + } + // Create approve and deny buttons + const approve = new ButtonBuilder() + .setCustomId('submission_approve_1234') + .setLabel('Approve') + .setStyle(ButtonStyle.Success); + + const deny = new ButtonBuilder() + .setCustomId('submission_deny_1234') + .setLabel('Deny') + .setStyle(ButtonStyle.Danger); + + const row = new ActionRowBuilder().addComponents(approve, deny); await channel.send({ - content: `New item submitted:\nName: ${name}\nValue: ${value}\nProof: ${proof}`, + embeds: [embed], + components: [row], }); } diff --git a/src/commands/event/team.js b/src/commands/event/team.js new file mode 100644 index 0000000..85960b1 --- /dev/null +++ b/src/commands/event/team.js @@ -0,0 +1,59 @@ +import { ChatInputCommandInteraction, SlashCommandBuilder } from 'discord.js'; +import { TeamService } from '#services/teamService.js'; +import logger from '#utils/logger.js'; +import { getColorFromName } from '#utils/color.js'; + +export const data = new SlashCommandBuilder() + .setName('team') + .setDescription('Get Team information') + .addStringOption((option) => + option + .setName('team') + .setDescription('Select the team to get information about') + .setRequired(false) + .setAutocomplete(true) + ); + +/** + * @param {ChatInputCommandInteraction} interaction + */ +export async function execute(interaction) { + const teamId = interaction.options.getString('team'); + const team = teamId + ? await TeamService.getTeamById(teamId) + : await TeamService.getCurrentTeam(interaction.user.id); + 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 }, + { + name: 'Members', + value: + team.members + .filter((member) => member.user.discordId !== team.leader.discordId) + .map((member, index) => `${index + 1}. <@${member.user.discordId}>`) + .join('\n') || 'None', + inline: false, + }, + ], + color: getColorFromName(team.name), + }; + 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 }))); +} diff --git a/src/constants/settings.js b/src/constants/settings.js index 0532826..8a5646e 100644 --- a/src/constants/settings.js +++ b/src/constants/settings.js @@ -1,4 +1,5 @@ export const SettingTypes = { APPROVAL_CHANNEL: 'approval_channel', - ADMIN_ROLE: 'admin_role', + ACCEPTED_CHANNEL: 'accepted_channel', + SIGNED_UP_CHANNEL: 'signed_up_channel', }; diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index 42ab80c..5e30bd0 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -1,4 +1,5 @@ -import { Events, MessageFlags, ChatInputCommandInteraction } from 'discord.js'; +import { Events, MessageFlags, ChatInputCommandInteraction, EmbedBuilder } from 'discord.js'; +import { ConfigService } from '#services/configService.js'; import logger from '#utils/logger.js'; export const name = Events.InteractionCreate; @@ -39,4 +40,44 @@ export async function execute(interaction) { }); } } + if (interaction.isButton()) { + const [event, action, id] = interaction.customId.split('_'); + if (event !== 'submission') return; + logger.info( + `Button clicked: ${action} - ${id} by ${interaction.user.username} (${interaction.user.id})` + ); + + // TODO: Update db with the action and id + + // Update embed to include approval status + const originalEmbed = interaction.message.embeds[0]; + if (!originalEmbed) return; + const embed = EmbedBuilder.from(originalEmbed); // Convert to EmbedBuilder if not already + embed.setColor(action === 'approve' ? '#00FF00' : '#FF0000'); // Set color based on action + const fields = embed.data.fields ?? []; + const index = fields.findIndex((field) => field.name === 'Status'); + if (index === -1) { + fields.push({ + name: 'Status', + value: `${action} by <@${interaction.user.id}>`, + inline: false, + }); + } else { + fields[index].value = `${action} by <@${interaction.user.id}>`; + } + embed.setFields(fields); + await interaction.update({ embeds: [embed] }); + + // If approved, submit to accepted channel + if (action === 'approve') { + const acceptedChannelId = await ConfigService.getAcceptedChannel(interaction.guildId); + if (!acceptedChannelId) return; + const acceptedChannel = await interaction.client.channels.fetch(acceptedChannelId); + if (acceptedChannel) { + await acceptedChannel.send({ + embeds: [embed], + }); + } + } + } } diff --git a/src/services/configService.js b/src/services/configService.js index 6905e25..29a5037 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -69,41 +69,104 @@ export class ConfigService { } /** - * Get the admin role for a guild + * Get the accepted channel for a guild + */ + static async getAcceptedChannel(guildId) { + try { + const config = await prisma.config.findUnique({ + where: { + guildId_settingType: { + guildId, + settingType: SettingTypes.ACCEPTED_CHANNEL, + }, + }, + }); + + return config?.value; + } catch (error) { + logger.error(`Error getting accepted channel for guild ${guildId}:`, error); + throw error; + } + } + + /** + * Set the accepted channel for a guild + * @param {String} guildId - A Discord guild ID + * @param {String} channelId - The accepted channel ID + * @returns {Promise} + */ + static async setAcceptedChannel(guildId, channelId) { + try { + const existingConfig = await prisma.config.findUnique({ + where: { + guildId_settingType: { + guildId, + settingType: SettingTypes.ACCEPTED_CHANNEL, + }, + }, + }); + + if (existingConfig) { + await prisma.config.update({ + where: { + guildId_settingType: { + guildId, + settingType: SettingTypes.ACCEPTED_CHANNEL, + }, + }, + data: { value: channelId }, + }); + } else { + await prisma.config.create({ + data: { + guildId, + settingType: SettingTypes.ACCEPTED_CHANNEL, + value: channelId, + }, + }); + } + } catch (error) { + logger.error(`Error setting accepted channel for guild ${guildId}:`, error); + throw error; + } + } + + /** + * Get the signed up channel for a guild * @param {String} guildId - A Discord guild ID - * @returns {Promise} The admin role ID + * @returns {Promise} The signed up channel ID */ - static async getAdminRole(guildId) { + static async getSignedUpChannel(guildId) { try { const config = await prisma.config.findUnique({ where: { guildId_settingType: { guildId, - settingType: SettingTypes.ADMIN_ROLE, + settingType: SettingTypes.SIGNED_UP_CHANNEL, }, }, }); return config?.value; } catch (error) { - logger.error(`Error getting admin role for guild ${guildId}:`, error); + logger.error(`Error getting signed up channel for guild ${guildId}:`, error); throw error; } } /** - * Set the admin role for a guild + * Set the signed up channel for a guild * @param {String} guildId - A Discord guild ID - * @param {String} roleId - The admin role ID + * @param {String} channelId - The signed up channel ID * @returns {Promise} */ - static async setAdminRole(guildId, roleId) { + static async setSignedUpChannel(guildId, channelId) { try { const existingConfig = await prisma.config.findUnique({ where: { guildId_settingType: { guildId, - settingType: SettingTypes.ADMIN_ROLE, + settingType: SettingTypes.SIGNED_UP_CHANNEL, }, }, }); @@ -113,22 +176,22 @@ export class ConfigService { where: { guildId_settingType: { guildId, - settingType: SettingTypes.ADMIN_ROLE, + settingType: SettingTypes.SIGNED_UP_CHANNEL, }, }, - data: { value: roleId }, + data: { value: channelId }, }); } else { await prisma.config.create({ data: { guildId, - settingType: SettingTypes.ADMIN_ROLE, - value: roleId, + settingType: SettingTypes.SIGNED_UP_CHANNEL, + value: channelId, }, }); } } catch (error) { - logger.error(`Error setting admin role for guild ${guildId}:`, error); + logger.error(`Error setting signed up channel for guild ${guildId}:`, error); throw error; } } diff --git a/src/services/eventService.js b/src/services/eventService.js index a7a3d66..fd428e0 100644 --- a/src/services/eventService.js +++ b/src/services/eventService.js @@ -68,6 +68,31 @@ export class EventService { } } + /** + * Get an event by ID + * @param {String} eventId - The ID of the event + * @return {Promise} 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 @@ -95,4 +120,181 @@ 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; + } + } + + /** + * Check if a user is a registered member of an active event + * @param {String} discordId - The Discord ID of the user + */ + static async isUserRegistered(discordId) { + try { + const event = await prisma.event.findFirst({ + where: { + active: true, + participants: { + some: { + user: { + discordId, + }, + status: 'REGISTERED', + }, + }, + }, + }); + return !!event; + } catch (error) { + logger.error(`Error checking if user ${discordId} is registered:`, error); + throw error; + } + } + + /** + * Register a user to the active event + * @param {String} discordId - The Discord ID of the user + * @param {String} rsn - The RuneScape name of the user + * @param {String} duo - (Optional) The RuneScape name of the user's duo partner + */ + static async registerUserToEvent(discordId, rsn, duo) { + try { + const activeEvent = await this.getActiveEvent(); + if (!activeEvent) { + throw new Error('No active event found'); + } + const participant = await prisma.eventParticipant.create({ + data: { + status: 'REGISTERED', + rsn: rsn?.toLocaleLowerCase(), + note: duo?.toLocaleLowerCase(), + user: { connect: { discordId } }, + event: { connect: { id: activeEvent.id } }, + }, + }); + return participant; + } catch (error) { + logger.error(`Error registering user ${discordId} to event:`, error); + throw error; + } + } + + /** + * Update user's RSN in the active event + * @param {String} discordId - The Discord ID of the user + */ + static async updateUserRsn(discordId, rsn, duo) { + try { + const activeEvent = await this.getActiveEvent(); + if (!activeEvent) { + throw new Error('No active event found'); + } + const data = { + rsn: rsn?.toLocaleLowerCase(), + }; + if (duo) { + data.note = duo.toLocaleLowerCase(); + } + const participant = await prisma.eventParticipant.update({ + where: { + userId_eventId: { + userId: discordId, + eventId: activeEvent.id, + }, + }, + data, + }); + return participant; + } catch (error) { + logger.error(`Error updating RSN for user ${discordId}:`, error); + throw error; + } + } + + static async getUserDetails(discordId) { + try { + const activeEvent = await this.getActiveEvent(); + if (!activeEvent) { + throw new Error('No active event found'); + } + const participant = await prisma.eventParticipant.findFirst({ + where: { + userId: discordId, + eventId: activeEvent.id, + }, + }); + return participant; + } catch (error) { + logger.error(`Error getting user details for ${discordId}:`, error); + throw error; + } + } } diff --git a/src/services/teamService.js b/src/services/teamService.js index 0a36f8b..815b85f 100644 --- a/src/services/teamService.js +++ b/src/services/teamService.js @@ -7,7 +7,7 @@ export class TeamService { * @param {String} leaderId - (Optional) The ID of the team leader * @param {String} name - The name of the team * @param {String} description - The description of the team - * @returns {Promise} + * @returns {Promise} The created team object */ static async createTeam(leaderId, name, description) { // Validate inputs @@ -16,7 +16,7 @@ export class TeamService { } // Create the team try { - await prisma.team.create({ + return await prisma.team.create({ data: { name, description, @@ -68,11 +68,19 @@ export class TeamService { throw error; } } + /** + * @typedef {import('@prisma/client').Team & { + * leader: import('@prisma/client').DiscordUser, + * members: (import('@prisma/client').TeamMember & { + * user: import('@prisma/client').DiscordUser + * })[] + * }} TeamWithDetails + */ /** * Get a team by ID * @param {String} teamId - The ID of the team - * @returns {Promise} The team object + * @returns {Promise} The team object */ static async getTeamById(teamId) { try { @@ -80,6 +88,14 @@ export class TeamService { where: { id: teamId, }, + include: { + leader: true, + members: { + include: { + user: true, + }, + }, + }, }); if (!team) { throw new Error('Team not found'); @@ -90,6 +106,40 @@ export class TeamService { throw error; } } + + /** + * Get current team of a user + * @param {String} discordId - The ID of the user + * @returns {Promise} The team object + */ + static async getCurrentTeam(discordId) { + try { + const team = await prisma.team.findFirst({ + where: { + members: { + some: { + user: { + discordId, + }, + }, + }, + }, + include: { + leader: true, + members: { + include: { + user: true, + }, + }, + }, + }); + return team; + } catch (error) { + logger.error('Error getting current team', error); + throw error; + } + } + /** * Update a team by ID * @param {String} teamId - The ID of the team @@ -121,4 +171,79 @@ export class TeamService { throw error; } } + + /** + * Check if a user is in a team + * @param {String} discordId - The discord Id of the user + */ + static async isUserInTeam(discordId) { + try { + const team = await prisma.team.findFirst({ + where: { + members: { + some: { + user: { + discordId, + }, + }, + }, + }, + }); + return !!team; + } catch (error) { + logger.error('Error checking if user is in team', error); + throw error; + } + } + + /** + * Add a member to a team + * @param {String} teamId - The ID of the team + * @param {String} userId - The ID of the user + * @returns {Promise} + */ + static async addMemberToTeam(teamId, userId) { + try { + return await prisma.team.update({ + where: { + id: teamId, + }, + data: { + members: { + create: { + user: { + connect: { + discordId: userId, + }, + }, + }, + }, + }, + }); + } catch (error) { + console.log(error); + logger.error('Error adding member to team', error); + throw error; + } + } + + /** + * Remove member from all teams + * @param {String} userId - The ID of the user + * @returns {Promise} + */ + static async removeMemberFromAllTeams(userId) { + try { + await prisma.teamMember.deleteMany({ + where: { + user: { + discordId: userId, + }, + }, + }); + } catch (error) { + logger.error('Error removing member from all teams', error); + throw error; + } + } } diff --git a/src/utils/color.js b/src/utils/color.js new file mode 100644 index 0000000..ec849b7 --- /dev/null +++ b/src/utils/color.js @@ -0,0 +1,10 @@ +export function getColorFromName(name) { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + + // Convert hash to hex color + const color = (hash & 0x00ffffff).toString(16).toUpperCase(); + return parseInt(color.padStart(6, '0'), 16); +}