From 0ee9766908ecad42c7697df676d81f950dff5206 Mon Sep 17 00:00:00 2001 From: Icheng Lin Date: Tue, 31 Oct 2023 04:42:59 -0500 Subject: [PATCH] Added Upcoming Events Command --- commands/command_roster.ts | 44 +++++--------- commands/command_upcoming.ts | 72 +++++++++++++++++++++++ index.ts | 2 + interactions/guild.ts | 22 +++++++ objects/robotevent.ts | 108 +++++++++++++++++++++++++++++------ utilities/flag.ts | 10 ++++ 6 files changed, 208 insertions(+), 50 deletions(-) create mode 100644 commands/command_upcoming.ts create mode 100644 utilities/flag.ts diff --git a/commands/command_roster.ts b/commands/command_roster.ts index 756aa89..933737f 100644 --- a/commands/command_roster.ts +++ b/commands/command_roster.ts @@ -1,10 +1,7 @@ import { ChatInputCommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js"; import VerificationCommand from "../templates/template_command"; import VerificationGuild from "../interactions/guild"; -import RobotEvent, { TeamData } from "../objects/robotevent"; - -import CountryCode from "../data/country_code.json"; -import { VerificationUserData } from "../interactions/user"; +import CountryFlag from "../utilities/flag"; export default class RosterCommand extends VerificationCommand { @@ -18,8 +15,8 @@ export default class RosterCommand extends VerificationCommand { public async command_trigger(command_interaction: ChatInputCommandInteraction): Promise { await command_interaction.deferReply(); // get users - const guild_users = await VerificationGuild.users_get(command_interaction.guild?.id as string); - if (guild_users.length <= 0) { + const guild_teams = await VerificationGuild.teams_get(command_interaction.guild?.id as string); + if (guild_teams.length <= 0) { // no registered user in guild const invalid_embed = new EmbedBuilder() .setTitle("⛔ Insufficient Members ⛔") @@ -28,36 +25,21 @@ export default class RosterCommand extends VerificationCommand { await command_interaction.editReply({embeds: [invalid_embed]}); return; } - // process teams - const guild_teams_raw = new Map(); - for (const loop_user of guild_users) { - if (guild_teams_raw.has(loop_user.user_team_id)) guild_teams_raw.get(loop_user.user_team_id)?.team_users.push(loop_user); - else guild_teams_raw.set(loop_user.user_team_id, { - team_number: loop_user.user_team_number, - team_data: await RobotEvent.get_team_by_number(loop_user.user_team_number) as TeamData, - team_users: [loop_user] - }); - } - const guild_teams = Array.from(guild_teams_raw, ([team_id, team_data]) => ({team_id, ...team_data})); // generate embed const roster_embed = new EmbedBuilder() .setTitle(`📙 ${command_interaction.guild?.name}'s Roster 📙`) .setDescription(`**${command_interaction.guild?.name}** had a total of **${guild_teams.length} registered teams**, below are the teams and their members.\n\u200B`) .addFields( - ...guild_teams.sort((team_a, team_b) => team_a.team_number.localeCompare(team_b.team_number)).map((loop_team) => { - const team_country_code = CountryCode.find(country_data => country_data.name === loop_team.team_data.team_country)?.code; - const team_country_flag = team_country_code !== undefined ? `:flag_${team_country_code.toLowerCase()}:` : ":earth_americas:"; - return { - name: `${loop_team.team_number}`, - value: [ - `\`${loop_team.team_data.team_name}\``, - `<:vrc_dot_blue:1135437387619639316> Country: ${team_country_flag}`, - `<:vrc_dot_blue:1135437387619639316> Grade: \`${loop_team.team_data.team_grade}\``, - ...loop_team.team_users.map(loop_user => `<@${loop_user.user_id}>`) - ].join("\n"), - inline: true - } - })) + ...guild_teams.sort((team_a, team_b) => team_a.team_number.localeCompare(team_b.team_number)).map((loop_team) => ({ + name: `${loop_team.team_number}`, + value: [ + `\`${loop_team.team_data.team_name}\``, + `<:vrc_dot_blue:1135437387619639316> Country: ${CountryFlag.get_flag(loop_team.team_data.team_country)}`, + `<:vrc_dot_blue:1135437387619639316> Grade: \`${loop_team.team_data.team_grade}\``, + ...loop_team.team_users.map(loop_user => `<@${loop_user.user_id}>`) + ].join("\n"), + inline: true + }))) .setTimestamp() .setFooter({text: `requested by ${command_interaction.user.tag}`, iconURL: command_interaction.client.user.displayAvatarURL()}) .setColor("#84cc16"); diff --git a/commands/command_upcoming.ts b/commands/command_upcoming.ts new file mode 100644 index 0000000..9452b1c --- /dev/null +++ b/commands/command_upcoming.ts @@ -0,0 +1,72 @@ +import { ChatInputCommandInteraction, EmbedBuilder, SlashCommandBuilder } from "discord.js"; +import VerificationGuild, { VerificationTeamData } from "../interactions/guild"; +import RobotEvent from "../objects/robotevent"; +import VerificationCommand from "../templates/template_command"; +import CountryFlag from "../utilities/flag"; + +export default class UpcomingCommand extends VerificationCommand { + + public command_configuration(): SlashCommandBuilder { + const command_builder = new SlashCommandBuilder() + .setName("upcoming") + .setDescription("Retrieves all the upcoming events of the teams in guild.") + .setDMPermission(false); + command_builder.addStringOption(option => option + .setName("team") + .setDescription("(Optional) Filter by the number of the team.") + .setMaxLength(8) + .setMinLength(2) + .setRequired(false) + ); + return command_builder; + } + + public async command_trigger(command_interaction: ChatInputCommandInteraction): Promise { + await command_interaction.deferReply(); + // get users + const guild_teams = await VerificationGuild.teams_get(command_interaction.guild?.id as string); + if (guild_teams.length <= 0) { + // no registered user in guild + const invalid_embed = new EmbedBuilder() + .setTitle("⛔ Insufficient Members ⛔") + .setDescription(`The command requires at least **one verified user** in **${command_interaction.guild?.name}** to display the events! If you believe this is in error, please contact an administrator.`) + .setColor("#ef4444"); + await command_interaction.editReply({embeds: [invalid_embed]}); + return; + } + const guild_registered_teams = guild_teams.reduce((value_previous, value_current) => ({...value_previous, [value_current.team_data.team_id]: value_current}), {} as {[key: number]: VerificationTeamData}); + let guild_events = await RobotEvent.get_guild_events((command_interaction.guild?.id as string), guild_teams.map(team_data => team_data.team_data.team_id), new Date()); + const guild_events_total = guild_events.length; + const guild_events_maximum = 10; + guild_events = guild_events.slice(0, Math.min(guild_events_total, guild_events_maximum)); + const guild_events_teams = await Promise.all(guild_events.map(event_data => RobotEvent.get_event_teams(event_data.event_id))); + // embed + const events_embed = new EmbedBuilder() + .setTitle(`🗓️ ${command_interaction.guild?.name}'s Upcoming Events 🗓️`) + .setDescription(`**${command_interaction.guild?.name}** had a total of **${guild_events_total} registered events**, below are the details of their **${guild_events_maximum} upcoming events**.\n\u200B`) + .addFields( + ...guild_events.map((event_data, event_index) => { + const event_location = [ + event_data.event_location.address_city, + event_data.event_location.address_state, + event_data.event_location.address_country + ].filter(address_component => address_component != null).join(", "); + const event_teams_guild = guild_events_teams[event_index].filter(team_data => guild_registered_teams[team_data.team_id] !== undefined); + const event_teams_excluded = guild_events_teams[event_index].length - event_teams_guild.length; + return { + name: `📌 ${event_data.event_name} 📌`, + value: [ + `<:vrc_dot_blue:1135437387619639316> Address: ${CountryFlag.get_flag(event_data.event_location.address_country)} \`${event_location}\``, + `<:vrc_dot_blue:1135437387619639316> Date: `, + `<:vrc_dot_blue:1135437387619639316> Teams: \`${event_teams_guild.map(team_data => team_data.team_number).join("\`, \`")}\` and \`${event_teams_excluded}\` more team(s)...`, + `<:vrc_dot_blue:1135437387619639316> Links: [**\`Robot Event\`**](https://www.robotevents.com/robot-competitions/vex-robotics-competition/${event_data.event_sku}.html)`, + ].join("\n") + } + })) + .setTimestamp() + .setFooter({text: `requested by ${command_interaction.user.tag}`, iconURL: command_interaction.client.user.displayAvatarURL()}) + .setColor("#84cc16"); + await command_interaction.editReply({embeds: [events_embed]}); + } + +} \ No newline at end of file diff --git a/index.ts b/index.ts index 8a7461f..48f2c63 100644 --- a/index.ts +++ b/index.ts @@ -15,6 +15,7 @@ import Registry from "./objects/registry"; import SkillsCommand from "./commands/command_skills"; import HelpButton from "./commands/button_help"; import RosterCommand from "./commands/command_roster"; +import UpcomingCommand from "./commands/command_upcoming"; dotenv.config(); @@ -37,6 +38,7 @@ verification_registry.register(new NickCommand); verification_registry.register(new AwardsCommand); verification_registry.register(new SkillsCommand); verification_registry.register(new RosterCommand); +verification_registry.register(new UpcomingCommand); verification_registry.register(new VerifyButton); verification_registry.register(new HelpButton); verification_registry.register(new VerifyModal); diff --git a/interactions/guild.ts b/interactions/guild.ts index 2112b04..325d2a4 100644 --- a/interactions/guild.ts +++ b/interactions/guild.ts @@ -1,11 +1,33 @@ import Database from "../objects/database"; +import RobotEvent, { TeamData } from "../objects/robotevent"; import { VerificationUserData } from "./user"; export default class VerificationGuild { + public static async teams_get(guild_id: string): Promise { + const guild_users = await VerificationGuild.users_get(guild_id); + // process teams + const guild_teams_raw = new Map(); + for (const loop_user of guild_users) { + if (guild_teams_raw.has(loop_user.user_team_id)) guild_teams_raw.get(loop_user.user_team_id)?.team_users.push(loop_user); + else guild_teams_raw.set(loop_user.user_team_id, { + team_number: loop_user.user_team_number, + team_data: await RobotEvent.get_team_by_number(loop_user.user_team_number) as TeamData, + team_users: [loop_user] + }); + } + return Array.from(guild_teams_raw, ([team_id, team_data]) => ({team_id, ...team_data})); + } + public static async users_get(guild_id: string): Promise { const database_matches = await Database.query(`SELECT * FROM verified_user_data WHERE guild_id=${guild_id}`); return database_matches; } +} + +export interface VerificationTeamData { + team_number: string, + team_data: TeamData, + team_users: VerificationUserData[] } \ No newline at end of file diff --git a/objects/robotevent.ts b/objects/robotevent.ts index 2f59728..c699892 100644 --- a/objects/robotevent.ts +++ b/objects/robotevent.ts @@ -72,26 +72,26 @@ export default class RobotEvent { const api_cache = await VerificationCache.cache_get(`ROBOTEVENT_SEASONSKILLS_${season_id}`); if (api_cache !== undefined) return api_cache.cache_data; // cache not exist - const api_response = (await this.fetch_retries(`https://www.robotevents.com/api/seasons/${season_id}/skills?grade_level=${encodeURI(grade_level)}`, 5).then(response => response.json())) as any[]; + const api_response = (await this.fetch_retries(`https://www.robotevents.com/api/seasons/${season_id}/skills?grade_level=${grade_level}`, 5).then(response => response.json())) as any[]; if ((api_response as any).message !== undefined) return []; const result = api_response.map(skill_data => ({ - skills_rank: skill_data.rank, - skills_entries: api_response.length, + skills_rank: skill_data.rank, + skills_entries: api_response.length, skills_team: { - team_id: skill_data.team.id, - team_number: skill_data.team.team, - team_name: skill_data.team.teamName, - team_organization: skill_data.team.organization, - team_country: skill_data.team.country, - team_program: skill_data.team.program, - team_grade: skill_data.team.gradeLevel + team_id: skill_data.team.id, + team_number: skill_data.team.team, + team_name: skill_data.team.teamName, + team_organization: skill_data.team.organization, + team_country: skill_data.team.country, + team_program: skill_data.team.program, + team_grade: skill_data.team.gradeLevel }, skills_score: { - driver_score: skill_data.scores.driver, - driver_time_stop: skill_data.scores.driverStopTime, - driver_score_date: skill_data.scores.driverScoredAt, - programming_score: skill_data.scores.programming, - programming_time_stop: skill_data.scores.progStopTime, + driver_score: skill_data.scores.driver, + driver_time_stop: skill_data.scores.driverStopTime, + driver_score_date: skill_data.scores.driverScoredAt, + programming_score: skill_data.scores.programming, + programming_time_stop: skill_data.scores.progStopTime, programming_score_date: skill_data.scores.progScoredAt, } } as SeasonSkills)); @@ -99,6 +99,53 @@ export default class RobotEvent { return result; } + public static async get_event_teams(event_id: number): Promise { + // load cache + const api_cache = await VerificationCache.cache_get(`ROBOTEVENT_EVENTTEAMS_${event_id}`); + if (api_cache !== undefined) return api_cache.cache_data; + // cache not exist + const result = (await this.get_response(`events/${event_id}/teams?per_page=1000`)).map((team_data: any) => ({ + team_id: team_data.id, + team_number: team_data.number, + team_name: team_data.team_name, + team_organization: team_data.organization, + team_country: (team_data.location === undefined) || (team_data.location.country), + team_program: (team_data.program === undefined) || (team_data.program.name), + team_grade: team_data.grade + } as TeamData)); + await VerificationCache.cache_set(`ROBOTEVENT_EVENTTEAMS_${event_id}`, result); + return result; + } + + public static async get_guild_events(guild_id: string, team_ids: number[], event_after: Date): Promise { + // load cache + const api_cache = await VerificationCache.cache_get(`ROBOTEVENT_GUILDEVENTS_${guild_id}`); + if (api_cache !== undefined) return api_cache.cache_data; + // cache not exist + const result = (await this.get_response(`events?${team_ids.map(team_id => `team[]=${team_id}`).join("&")}&start=${event_after.toISOString()}&per_page=1000`)).map((event_data: any) => ({ + event_id: event_data.id, + event_sku: event_data.sku, + event_name: event_data.name, + event_date: { + date_begin: event_data.start, + date_end: event_data.end + }, + event_program: { + program_id: event_data.program.id, + program_name: event_data.program.name + }, + event_location: { + address_lines: [event_data.location.address_1, event_data.location.address_2].filter(address_line => address_line != null), + address_city: event_data.location.city, + address_state: event_data.location.region, + address_postcode: event_data.location.postcode, + address_country: event_data.location.country, + } + } as EventData)); + await VerificationCache.cache_set(`ROBOTEVENT_GUILDEVENTS_${guild_id}`, result); + return result; + } + private static async get_response(api_path: string): Promise { const api_response = await this.fetch_retries(`https://www.robotevents.com/api/v2/${api_path}`, 5).then(response => response.json()) as any; if (api_response.data.length <= 0) return []; @@ -115,7 +162,7 @@ export default class RobotEvent { private static async fetch_retries(request_url: string, retry_amount: number): Promise { for (let attempt_index = 0; attempt_index < (retry_amount + 1); attempt_index++) { try { - return await fetch(request_url, {headers: this.get_authorization()}); + return await fetch(encodeURI(request_url), {headers: this.get_authorization()}); } catch (error) { Logger.send_log(`Request to ${request_url} failed, attempt refetch #${attempt_index + 1}`); } @@ -141,20 +188,43 @@ export interface TeamData { team_grade: string } -export interface EventData { +export interface EventDataSimplified { event_id: number, event_name: string } +export interface EventData { + event_id: number, + event_sku: string, + event_name: string, + event_date: { + date_begin: string, + date_end: string + }, + event_program: { + program_id: number, + program_name: string + }, + event_location: LocationData +} + export interface SeasonData { season_id: number, season_name: string } +export interface LocationData { + address_lines: string[], + address_city: string, + address_state: string, + address_postcode: string, + address_country: string, +} + export interface TeamAward { award_id: number, award_name: string, - award_event: EventData + award_event: EventDataSimplified } export interface TeamSkills { @@ -163,7 +233,7 @@ export interface TeamSkills { skill_score: number, skill_rank: number, skill_attempts: number, - skill_event: EventData, + skill_event: EventDataSimplified, skill_season: SeasonData } diff --git a/utilities/flag.ts b/utilities/flag.ts new file mode 100644 index 0000000..402c460 --- /dev/null +++ b/utilities/flag.ts @@ -0,0 +1,10 @@ +import CountryCode from "../data/country_code.json"; + +export default class CountryFlag { + + public static get_flag(country_name: string): string { + const team_country_code = CountryCode.find(country_data => country_data.name === country_name)?.code; + return (team_country_code !== undefined) ? `:flag_${team_country_code.toLowerCase()}:` : ":earth_americas:"; + } + +} \ No newline at end of file