Skip to content

Commit

Permalink
Added Upcoming Events Command
Browse files Browse the repository at this point in the history
  • Loading branch information
ichenglin committed Oct 31, 2023
1 parent 39fe211 commit 0ee9766
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 50 deletions.
44 changes: 13 additions & 31 deletions commands/command_roster.ts
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -18,8 +15,8 @@ export default class RosterCommand extends VerificationCommand {
public async command_trigger(command_interaction: ChatInputCommandInteraction): Promise<void> {
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 ⛔")
Expand All @@ -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<number, {team_number: string, team_data: TeamData, team_users: VerificationUserData[]}>();
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");
Expand Down
72 changes: 72 additions & 0 deletions commands/command_upcoming.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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: <t:${Math.floor(new Date(event_data.event_date.date_begin).getTime() / 1000)}:R>`,
`<: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]});
}

}
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);
Expand Down
22 changes: 22 additions & 0 deletions interactions/guild.ts
Original file line number Diff line number Diff line change
@@ -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<VerificationTeamData[]> {
const guild_users = await VerificationGuild.users_get(guild_id);
// process teams
const guild_teams_raw = new Map<number, {team_number: string, team_data: TeamData, team_users: VerificationUserData[]}>();
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<VerificationUserData[]> {
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[]
}
108 changes: 89 additions & 19 deletions objects/robotevent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,33 +72,80 @@ 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));
await VerificationCache.cache_set(`ROBOTEVENT_SEASONSKILLS_${season_id}`, result);
return result;
}

public static async get_event_teams(event_id: number): Promise<TeamData[]> {
// 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<EventData[]> {
// 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<any[]> {
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 [];
Expand All @@ -115,7 +162,7 @@ export default class RobotEvent {
private static async fetch_retries(request_url: string, retry_amount: number): Promise<any> {
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}`);
}
Expand All @@ -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 {
Expand All @@ -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
}

Expand Down
10 changes: 10 additions & 0 deletions utilities/flag.ts
Original file line number Diff line number Diff line change
@@ -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:";
}

}

0 comments on commit 0ee9766

Please sign in to comment.