diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index ad690d2f..204f64cb 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -15,6 +15,7 @@ Update _ October 2024
- fix: Changing A32NX Releases to Aircraft Releases in Role assignment (30/10/2024)
- fix: Bugs with permissions causing crash during startup or prefix command handling (30/10/2024)
- feat: Prefix Command Management (28/10/2024)
+
- fix: role assignment typo for server announcements (22/10/2024)
Update _ August 2024
diff --git a/package-lock.json b/package-lock.json
index a10096ac..366c6ff6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,7 +21,8 @@
"moment": "^2.29.4",
"mongoose": "^8.0.3",
"node-fetch": "^2.6.10",
- "winston": "^3.3.4"
+ "winston": "^3.3.4",
+ "zod": "^3.23.8"
},
"devDependencies": {
"@flybywiresim/eslint-config": "^0.1.0",
@@ -7767,6 +7768,14 @@
"engines": {
"node": ">=6"
}
+ },
+ "node_modules/zod": {
+ "version": "3.23.8",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/package.json b/package.json
index ec58a2b3..b13d2209 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,8 @@
"moment": "^2.29.4",
"mongoose": "^8.0.3",
"node-fetch": "^2.6.10",
- "winston": "^3.3.4"
+ "winston": "^3.3.4",
+ "zod": "^3.23.8"
},
"devDependencies": {
"@flybywiresim/eslint-config": "^0.1.0",
diff --git a/src/commands/utils/liveFlights.ts b/src/commands/utils/liveFlights.ts
index cc0e0b96..8d4e3844 100644
--- a/src/commands/utils/liveFlights.ts
+++ b/src/commands/utils/liveFlights.ts
@@ -1,5 +1,6 @@
import { ApplicationCommandType, Colors } from 'discord.js';
-import { slashCommand, slashCommandStructure, makeEmbed, Logger } from '../../lib';
+import { ZodError } from 'zod';
+import { slashCommand, slashCommandStructure, makeEmbed, Logger, fetchForeignAPI, TelexCountSchema } from '../../lib';
const data = slashCommandStructure({
name: 'live-flights',
@@ -11,8 +12,10 @@ const FBW_WEB_MAP_URL = 'https://flybywiresim.com/map';
const FBW_API_BASE_URL = 'https://api.flybywiresim.com';
export default slashCommand(data, async ({ interaction }) => {
+ await interaction.deferReply();
+
try {
- const flights = await fetch(`${FBW_API_BASE_URL}/txcxn/_count`).then((res) => res.json());
+ const flights = await fetchForeignAPI(`${FBW_API_BASE_URL}/txcxn/_count`, TelexCountSchema);
const flightsEmbed = makeEmbed({
title: 'Live Flights',
description: `There are currently **${flights}** active flights with TELEX enabled.`,
@@ -20,8 +23,16 @@ export default slashCommand(data, async ({ interaction }) => {
url: FBW_WEB_MAP_URL,
timestamp: new Date().toISOString(),
});
- return interaction.reply({ embeds: [flightsEmbed] });
+ return interaction.editReply({ embeds: [flightsEmbed] });
} catch (e) {
+ if (e instanceof ZodError) {
+ const errorEmbed = makeEmbed({
+ title: 'TELEX Error',
+ description: 'The API returned unknown data.',
+ color: Colors.Red,
+ });
+ return interaction.editReply({ embeds: [errorEmbed] });
+ }
const error = e as Error;
Logger.error(error);
const errorEmbed = makeEmbed({
@@ -29,6 +40,6 @@ export default slashCommand(data, async ({ interaction }) => {
description: error.message,
color: Colors.Red,
});
- return interaction.reply({ embeds: [errorEmbed] });
+ return interaction.editReply({ embeds: [errorEmbed] });
}
});
diff --git a/src/commands/utils/metar.ts b/src/commands/utils/metar.ts
index dd688e48..2cc42e3f 100644
--- a/src/commands/utils/metar.ts
+++ b/src/commands/utils/metar.ts
@@ -1,6 +1,7 @@
import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js';
-import fetch from 'node-fetch';
-import { constantsConfig, slashCommand, slashCommandStructure, makeEmbed, makeLines, Logger } from '../../lib';
+import { Request } from 'node-fetch';
+import { ZodError } from 'zod';
+import { constantsConfig, fetchForeignAPI, makeEmbed, makeLines, slashCommand, slashCommandStructure, Metar, MetarSchema, Logger } from '../../lib';
const data = slashCommandStructure({
name: 'metar',
@@ -16,6 +17,12 @@ const data = slashCommandStructure({
}],
});
+const errorEmbed = (error: string) => makeEmbed({
+ title: 'METAR Error',
+ description: error,
+ color: Colors.Red,
+});
+
export default slashCommand(data, async ({ interaction }) => {
await interaction.deferReply();
@@ -32,55 +39,45 @@ export default slashCommand(data, async ({ interaction }) => {
return interaction.editReply({ embeds: [noTokenEmbed] });
}
+ let metar: Metar;
try {
- const metarReport: any = await fetch(`https://avwx.rest/api/metar/${icao}`, {
+ metar = await fetchForeignAPI(new Request(`https://avwx.rest/api/metar/${icao}`, {
method: 'GET',
headers: { Authorization: metarToken },
- })
- .then((res) => res.json());
-
- if (metarReport.error) {
- const invalidEmbed = makeEmbed({
- title: `Metar Error | ${icao.toUpperCase()}`,
- description: metarReport.error,
- color: Colors.Red,
- });
- return interaction.editReply({ embeds: [invalidEmbed] });
- }
- const metarEmbed = makeEmbed({
- title: `METAR Report | ${metarReport.station}`,
- description: makeLines([
- '**Raw Report**',
- metarReport.raw,
- '',
- '**Basic Report:**',
- `**Time Observed:** ${metarReport.time.dt}`,
- `**Station:** ${metarReport.station}`,
- `**Wind:** ${metarReport.wind_direction.repr}${metarReport.wind_direction.repr === 'VRB' ? '' : constantsConfig.units.DEGREES} at ${metarReport.wind_speed.repr} ${metarReport.units.wind_speed}`,
- `**Visibility:** ${metarReport.visibility.repr} ${Number.isNaN(+metarReport.visibility.repr) ? '' : metarReport.units.visibility}`,
- `**Temperature:** ${metarReport.temperature.repr} ${constantsConfig.units.CELSIUS}`,
- `**Dew Point:** ${metarReport.dewpoint.repr} ${constantsConfig.units.CELSIUS}`,
- `**Altimeter:** ${metarReport.altimeter.value.toString()} ${metarReport.units.altimeter}`,
- `**Flight Rules:** ${metarReport.flight_rules}`,
- ]),
- fields: [
- {
- name: 'Unsure of how to read the raw report?',
- value: 'Please refer to our guide [here.](https://docs.flybywiresim.com/pilots-corner/airliner-flying-guide/weather/)',
- inline: false,
- },
- ],
- footer: { text: 'This METAR report may not accurately reflect the weather in the simulator. However, it will always be similar to the current conditions present in the sim.' },
- });
-
- return interaction.editReply({ embeds: [metarEmbed] });
+ }), MetarSchema);
} catch (e) {
- Logger.error('metar:', e);
- const fetchErrorEmbed = makeEmbed({
- title: 'Metar Error | Fetch Error',
- description: 'There was an error fetching the METAR report. Please try again later.',
- color: Colors.Red,
- });
- return interaction.editReply({ embeds: [fetchErrorEmbed] });
+ if (e instanceof ZodError) {
+ return interaction.editReply({ embeds: [errorEmbed('The API returned unknown data.')] });
+ }
+ Logger.error(`Error occured while fetching METAR: ${String(e)}`);
+ return interaction.editReply({ embeds: [errorEmbed(`An error occurred while fetching the latest METAR for ${icao.toUpperCase()}.`)] });
}
+
+ const metarEmbed = makeEmbed({
+ title: `METAR Report | ${metar.station}`,
+ description: makeLines([
+ '**Raw Report**',
+ metar.raw,
+ '',
+ '**Basic Report:**',
+ `**Time Observed:** ${metar.time.dt}`,
+ `**Station:** ${metar.station}`,
+ `**Wind:** ${metar.wind_direction.repr}${metar.wind_direction.repr === 'VRB' ? '' : constantsConfig.units.DEGREES} at ${metar.wind_speed.repr} ${metar.units.wind_speed}`,
+ `**Visibility:** ${metar.visibility.repr} ${Number.isNaN(+metar.visibility.repr) ? '' : metar.units.visibility}`,
+ `**Temperature:** ${metar.temperature.repr} ${constantsConfig.units.CELSIUS}`,
+ `**Dew Point:** ${metar.dewpoint.repr} ${constantsConfig.units.CELSIUS}`,
+ `**Altimeter:** ${metar.altimeter.value.toString()} ${metar.units.altimeter}`,
+ `**Flight Rules:** ${metar.flight_rules}`,
+ ]),
+ fields: [
+ {
+ name: 'Unsure of how to read the raw report?',
+ value: 'Please refer to our guide [here](https://docs.flybywiresim.com/pilots-corner/airliner-flying-guide/weather/).',
+ inline: false,
+ },
+ ],
+ footer: { text: 'This METAR report may not accurately reflect the weather in the simulator. However, it will always be similar to the current conditions present in the sim.' },
+ });
+
+ return interaction.editReply({ embeds: [metarEmbed] });
});
diff --git a/src/commands/utils/simbriefData.ts b/src/commands/utils/simbriefData.ts
index b7d818a1..4fb47dd6 100644
--- a/src/commands/utils/simbriefData.ts
+++ b/src/commands/utils/simbriefData.ts
@@ -1,6 +1,7 @@
import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js';
import moment from 'moment';
-import { slashCommand, makeEmbed, makeLines, slashCommandStructure } from '../../lib';
+import { ZodError } from 'zod';
+import { fetchForeignAPI, Logger, makeEmbed, makeLines, SimbriefFlightPlan, SimbriefFlightPlanSchema, slashCommand, slashCommandStructure } from '../../lib';
const data = slashCommandStructure({
name: 'simbrief-data',
@@ -40,9 +41,9 @@ const simbriefdatarequestEmbed = makeEmbed({
]),
});
-const errorEmbed = (errorMessage: any) => makeEmbed({
+const errorEmbed = (error: string) => makeEmbed({
title: 'SimBrief Error',
- description: makeLines(['SimBrief data could not be read.', errorMessage]),
+ description: error,
color: Colors.Red,
});
@@ -53,10 +54,10 @@ const simbriefIdMismatchEmbed = (enteredId: any, flightplanId: any) => makeEmbed
]),
});
-const simbriefEmbed = (flightplan: any) => makeEmbed({
+const simbriefEmbed = (flightplan: SimbriefFlightPlan) => makeEmbed({
title: 'SimBrief Data',
description: makeLines([
- `**Generated at**: ${moment(flightplan.params.time_generated * 1000).format('DD.MM.YYYY, HH:mm:ss')}`,
+ `**Generated at**: ${moment(Number.parseInt(flightplan.params.time_generated) * 1000).format('DD.MM.YYYY, HH:mm:ss')}`,
`**AirFrame**: ${flightplan.aircraft.name} ${flightplan.aircraft.internal_id} ${(flightplan.aircraft.internal_id === FBW_AIRFRAME_ID) ? '(provided by FBW)' : ''}`,
`**AIRAC Cycle**: ${flightplan.params.airac}`,
`**Origin**: ${flightplan.origin.icao_code} ${flightplan.origin.plan_rwy}`,
@@ -71,23 +72,33 @@ export default slashCommand(data, async ({ interaction }) => {
return interaction.reply({ embeds: [simbriefdatarequestEmbed] });
}
+ await interaction.deferReply();
+
if (interaction.options.getSubcommand() === 'retrieve') {
const simbriefId = interaction.options.getString('pilot_id');
- if (!simbriefId) return interaction.reply({ content: 'Invalid pilot ID!', ephemeral: true });
+ if (!simbriefId) return interaction.editReply({ content: 'Invalid pilot ID!' });
- const flightplan = await fetch(`https://www.simbrief.com/api/xml.fetcher.php?json=1&userid=${simbriefId}&username=${simbriefId}`).then((res) => res.json());
+ let flightplan: SimbriefFlightPlan;
+ try {
+ flightplan = await fetchForeignAPI(`https://www.simbrief.com/api/xml.fetcher.php?json=1&userid=${simbriefId}&username=${simbriefId}`, SimbriefFlightPlanSchema);
+ } catch (e) {
+ if (e instanceof ZodError) {
+ return interaction.editReply({ embeds: [errorEmbed('The API returned unknown data.')] });
+ }
+ Logger.error(`Error while fetching SimBrief flightplan: ${String(e)}`);
+ return interaction.editReply({ embeds: [errorEmbed('An error occurred while fetching the SimBrief flightplan.')] });
+ }
if (flightplan.fetch.status !== 'Success') {
- interaction.reply({ embeds: [errorEmbed(flightplan.fetch.status)], ephemeral: true });
- return Promise.resolve();
+ return interaction.editReply({ embeds: [errorEmbed(flightplan.fetch.status)] });
}
if (!simbriefId.match(/\D/) && simbriefId !== flightplan.params.user_id) {
- interaction.reply({ embeds: [simbriefIdMismatchEmbed(simbriefId, flightplan.params.user_id)] });
+ return interaction.editReply({ embeds: [simbriefIdMismatchEmbed(simbriefId, flightplan.params.user_id)] });
}
- interaction.reply({ embeds: [simbriefEmbed(flightplan)] });
- return Promise.resolve();
+ return interaction.editReply({ embeds: [simbriefEmbed(flightplan)] });
}
- return Promise.resolve();
+
+ return interaction.editReply({ content: 'Unknown subcommand.' });
});
diff --git a/src/commands/utils/station.ts b/src/commands/utils/station.ts
index 8cde14a2..20275e08 100644
--- a/src/commands/utils/station.ts
+++ b/src/commands/utils/station.ts
@@ -1,6 +1,9 @@
import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js';
-import fetch from 'node-fetch';
-import { slashCommand, slashCommandStructure, makeEmbed, Logger, makeLines } from '../../lib';
+import { Request } from 'node-fetch';
+import { z, ZodError } from 'zod';
+import { AVWXRunwaySchema, AVWXStation, AVWXStationSchema, fetchForeignAPI, Logger, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib';
+
+type Runway = z.infer;
const data = slashCommandStructure({
name: 'station',
@@ -22,6 +25,12 @@ const noQueryEmbed = makeEmbed({
color: Colors.Red,
});
+const errorEmbed = (error: string) => makeEmbed({
+ title: 'Station Error',
+ description: error,
+ color: Colors.Red,
+});
+
export default slashCommand(data, async ({ interaction }) => {
await interaction.deferReply();
@@ -40,54 +49,44 @@ export default slashCommand(data, async ({ interaction }) => {
if (!icao) return interaction.editReply({ embeds: [noQueryEmbed] });
+ let station: AVWXStation;
try {
- const stationReport: any = await fetch(`https://avwx.rest/api/station/${icao}`, {
+ station = await fetchForeignAPI(new Request(`https://avwx.rest/api/station/${icao}`, {
method: 'GET',
headers: { Authorization: stationToken },
- }).then((res) => res.json());
-
- if (stationReport.error) {
- const invalidEmbed = makeEmbed({
- title: `Station Error | ${icao.toUpperCase()}`,
- description: stationReport.error,
- color: Colors.Red,
- });
- return interaction.editReply({ embeds: [invalidEmbed] });
+ }), AVWXStationSchema);
+ } catch (e) {
+ if (e instanceof ZodError) {
+ return interaction.editReply({ embeds: [errorEmbed('The API returned unknown data.')] });
}
+ Logger.error(`Error while fetching station info from AVWX: ${e}`);
+ return interaction.editReply({ embeds: [errorEmbed(`An error occurred while fetching the station information for ${icao.toUpperCase()}.`)] });
+ }
- const runwayIdents = stationReport.runways.map((runways: any) => `**${runways.ident1}/${runways.ident2}:** `
- + `${runways.length_ft} ft x ${runways.width_ft} ft / `
- + `${Math.round(runways.length_ft * 0.3048)} m x ${Math.round(runways.width_ft * 0.3048)} m`);
+ const runwayIdents = station.runways ? station.runways.map((runways: Runway) => `**${runways.ident1}/${runways.ident2}:** `
+ + `${runways.length_ft} ft x ${runways.width_ft} ft / `
+ + `${Math.round(runways.length_ft * 0.3048)} m x ${Math.round(runways.width_ft * 0.3048)} m`) : null;
- const stationEmbed = makeEmbed({
- title: `Station Info | ${stationReport.icao}`,
- description: makeLines([
- '**Station Information:**',
- `**Name:** ${stationReport.name}`,
- `**Country:** ${stationReport.country}`,
- `**City:** ${stationReport.city}`,
- `**Latitude:** ${stationReport.latitude}°`,
- `**Longitude:** ${stationReport.longitude}°`,
- `**Elevation:** ${stationReport.elevation_m} m/${stationReport.elevation_ft} ft`,
- '',
- '**Runways (Ident1/Ident2: Length x Width):**',
- `${runwayIdents.toString().replace(/,/g, '\n')}`,
- '',
- `**Type:** ${stationReport.type.replace(/_/g, ' ')}`,
- `**Website:** ${stationReport.website}`,
- `**Wiki:** ${stationReport.wiki}`,
- ]),
- footer: { text: 'Due to limitations of the API, not all links may be up to date at all times.' },
- });
+ const stationEmbed = makeEmbed({
+ title: `Station Info | ${station.icao}`,
+ description: makeLines([
+ '**Station Information:**',
+ `**Name:** ${station.name}`,
+ `**Country:** ${station.country}`,
+ `**City:** ${station.city}`,
+ `**Latitude:** ${station.latitude}°`,
+ `**Longitude:** ${station.longitude}°`,
+ `**Elevation:** ${station.elevation_m} m/${station.elevation_ft} ft`,
+ '',
+ '**Runways (Length x Width):**',
+ `${runwayIdents ? runwayIdents.toString().replace(/,/g, '\n') : 'N/A'}`,
+ '',
+ `**Type:** ${station.type.replace(/_/g, ' ')}`,
+ `**Website:** ${station.website ?? 'N/A'}`,
+ `**Wiki:** ${station.wiki ?? 'N/A'}`,
+ ]),
+ footer: { text: 'Due to limitations of the API, not all links may be up to date at all times.' },
+ });
- return interaction.editReply({ embeds: [stationEmbed] });
- } catch (error) {
- Logger.error('station:', error);
- const fetchErrorEmbed = makeEmbed({
- title: 'Station Error | Fetch Error',
- description: 'There was an error fetching the station report. Please try again later.',
- color: Colors.Red,
- });
- return interaction.editReply({ embeds: [fetchErrorEmbed] });
- }
+ return interaction.editReply({ embeds: [stationEmbed] });
});
diff --git a/src/commands/utils/taf.ts b/src/commands/utils/taf.ts
index dba64551..96caab98 100644
--- a/src/commands/utils/taf.ts
+++ b/src/commands/utils/taf.ts
@@ -1,6 +1,7 @@
import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js';
-import fetch from 'node-fetch';
-import { constantsConfig, slashCommand, slashCommandStructure, makeEmbed, makeLines, Logger } from '../../lib';
+import { Request } from 'node-fetch';
+import { ZodError } from 'zod';
+import { Logger, TAF, TafSchema, fetchForeignAPI, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib';
const data = slashCommandStructure({
name: 'taf',
@@ -22,6 +23,12 @@ const noQueryEmbed = makeEmbed({
color: Colors.Red,
});
+const errorEmbed = (error: string) => makeEmbed({
+ title: 'TAF Error',
+ description: error,
+ color: Colors.Red,
+});
+
export default slashCommand(data, async ({ interaction }) => {
await interaction.deferReply();
@@ -42,61 +49,37 @@ export default slashCommand(data, async ({ interaction }) => {
return interaction.editReply({ embeds: [noQueryEmbed] });
}
+ let taf: TAF;
try {
- const tafReport: any = await fetch(`https://avwx.rest/api/taf/${icao}`, {
+ taf = await fetchForeignAPI(new Request(`https://avwx.rest/api/taf/${icao}`, {
method: 'GET',
headers: { Authorization: tafToken },
- }).then((res) => res.json());
-
- if (tafReport.error) {
- const invalidEmbed = makeEmbed({
- title: `TAF Error | ${icao.toUpperCase()}`,
- description: tafReport.error,
- color: Colors.Red,
- });
- return interaction.editReply({ embeds: [invalidEmbed] });
+ }), TafSchema);
+ } catch (e) {
+ if (e instanceof ZodError) {
+ return interaction.editReply({ embeds: [errorEmbed('The API returned unknown data.')] });
}
- const getClouds = (clouds: any) => {
- const retClouds = [];
- for (const cloud of clouds) {
- retClouds.push(cloud.repr);
- }
- return retClouds.join(', ');
- };
- const tafEmbed = makeEmbed({
- title: `TAF Report | ${tafReport.station}`,
- description: makeLines([
- '**Raw Report**',
- tafReport.raw,
+ Logger.error(`Error while fetching TAF from AVWX: ${e}`);
+ return interaction.editReply({ embeds: [errorEmbed(`An error occurred while fetching the latest TAF for ${icao.toUpperCase()}.`)] });
+ }
- '',
- '**Basic Report:**',
- `**Time Forecasted:** ${tafReport.time.dt}`,
- `**Forecast Start Time:** ${tafReport.start_time.dt}`,
- `**Forecast End Time:** ${tafReport.end_time.dt}`,
- `**Visibility:** ${tafReport.forecast[0].visibility.repr} ${Number.isNaN(+tafReport.forecast[0].visibility.repr) ? '' : tafReport.units.visibility}`,
- `**Wind:** ${tafReport.forecast[0].wind_direction.repr}${tafReport.forecast[0].wind_direction.repr === 'VRB' ? '' : constantsConfig.units.DEGREES} at ${tafReport.forecast[0].wind_speed.repr} ${tafReport.units.wind_speed}`,
- `**Clouds:** ${getClouds(tafReport.forecast[0].clouds)}`,
- `**Flight Rules:** ${tafReport.forecast[0].flight_rules}`,
- ]),
- fields: [
- {
- name: 'Unsure of how to read the raw report?',
- value: 'Please refer to our guide [here.](https://docs.flybywiresim.com/pilots-corner/airliner-flying-guide/weather/#taf-example-decoded)',
- inline: false,
- },
- ],
- footer: { text: 'This TAF report is only a forecast, and may not accurately reflect weather in the simulator.' },
- });
+ const tafEmbed = makeEmbed({
+ title: `TAF Report | ${taf.station}`,
+ description: makeLines(['**Raw Report**', ...taf.forecast.map((forecast, i) => {
+ if (i === 0) {
+ return `${taf.station} ${forecast.raw}`;
+ }
+ return forecast.raw;
+ })]),
+ fields: [
+ {
+ name: 'Unsure of how to read the report?',
+ value: `Please refer to our guide [here](https://docs.flybywiresim.com/pilots-corner/airliner-flying-guide/weather/#taf-example-decoded) or see above report decoded [here](https://e6bx.com/weather/${taf.station}/?showDecoded=1&focuspoint=tafdecoder).`,
+ inline: false,
+ },
+ ],
+ footer: { text: 'This TAF report is only a forecast, and may not accurately reflect weather in the simulator.' },
+ });
- return interaction.editReply({ embeds: [tafEmbed] });
- } catch (error) {
- Logger.error('taf:', error);
- const fetchErrorEmbed = makeEmbed({
- title: 'TAF Error | Fetch Error',
- description: 'There was an error fetching the TAF report. Please try again later.',
- color: Colors.Red,
- });
- return interaction.editReply({ embeds: [fetchErrorEmbed] });
- }
+ return interaction.editReply({ embeds: [tafEmbed] });
});
diff --git a/src/commands/utils/vatsim/functions/vatsimControllers.ts b/src/commands/utils/vatsim/functions/vatsimControllers.ts
index 08e5cd2b..a90f7fb7 100644
--- a/src/commands/utils/vatsim/functions/vatsimControllers.ts
+++ b/src/commands/utils/vatsim/functions/vatsimControllers.ts
@@ -1,5 +1,9 @@
import { ChatInputCommandInteraction, EmbedField } from 'discord.js';
-import { makeEmbed } from '../../../../lib';
+import { z } from 'zod';
+import { VatsimAtisSchema, VatsimRatingSchema, VatsimData, makeEmbed } from '../../../../lib';
+
+type Atis = z.infer;
+type Rating = z.infer;
/* eslint-disable camelcase */
@@ -9,7 +13,7 @@ const listEmbed = (type: string, fields: EmbedField[], totalCount: number, shown
fields,
});
-const controllersListEmbedFields = (callsign: string, frequency: string, logon: string, rating: string, atis: string, atisCode: string): EmbedField[] => {
+const controllersListEmbedFields = (callsign: string, frequency: string, logon: string, rating?: Rating, atis?: Atis): EmbedField[] => {
const fields = [
{
name: 'Callsign',
@@ -26,22 +30,20 @@ const controllersListEmbedFields = (callsign: string, frequency: string, logon:
value: `${logon}`,
inline: true,
},
- {
+ ];
+
+ if (rating) {
+ fields.push({
name: 'Rating',
- value: `${rating}`,
+ value: `${rating.short} - ${rating.long}`,
inline: true,
- },
- ];
- if (atis !== null) {
- let atisTitle = 'Info';
- if (atisCode) {
- atisTitle = `ATIS - Code: ${atisCode}`;
- } else if (atisCode !== undefined) {
- atisTitle = 'ATIS';
- }
+ });
+ }
+
+ if (atis && atis.text_atis) {
fields.push({
- name: atisTitle,
- value: atis,
+ name: `ATIS - ${atis.atis_code ? atis.atis_code : 'N/A'}`,
+ value: atis.text_atis.join('\n'),
inline: false,
});
}
@@ -57,30 +59,17 @@ const handleLocaleDateTimeString = (date: Date) => date.toLocaleDateString('en-U
day: 'numeric',
});
-export async function handleVatsimControllers(interaction: ChatInputCommandInteraction<'cached'>, vatsimData: any, callsignSearch: any) {
- const vatsimAllControllers = vatsimData.controllers ? vatsimData.controllers.filter((controller: { facility: number; }) => controller.facility > 0) : null;
-
- const vatsimControllerRatings = vatsimData.ratings ? vatsimData.ratings : null;
- const vatsimControllers = vatsimAllControllers ? vatsimAllControllers.filter((controller: { callsign: string | string[]; }) => controller.callsign.includes(callsignSearch)) : null;
- const vatsimAtis = vatsimData.atis ? vatsimData.atis.filter((atis: { callsign: string | string[]; }) => atis.callsign.includes(callsignSearch)) : null;
-
- const { keys }: ObjectConstructor = Object;
-
- const fields: EmbedField[] = [...vatsimControllers.sort((a: { facility: number; }, b: { facility: number; }) => b.facility - a.facility), ...vatsimAtis]
- .map((vatsimController) => {
- const { callsign, frequency, logon_time, atis_code, text_atis, rating } = vatsimController;
- const logonTime = new Date(logon_time);
- const logonTimeString = handleLocaleDateTimeString(logonTime);
- const ratingDetail = vatsimControllerRatings.filter((ratingInfo: { id: any; }) => ratingInfo.id === rating);
- const { short, long } = ratingDetail[0];
- const ratingText = `${short} - ${long}`;
- const atisText = text_atis ? text_atis.join('\n') : null;
+export async function handleVatsimControllers(interaction: ChatInputCommandInteraction<'cached'>, vatsimData: VatsimData, callsignSearch: string) {
+ const controllers = vatsimData.controllers.filter((controller) => controller.facility > 0 && controller.callsign.includes(callsignSearch));
+ controllers.sort((a, b) => b.facility - a.facility);
- return controllersListEmbedFields(callsign, frequency, logonTimeString, ratingText, atisText, atis_code);
- }).slice(0, 5).flat();
+ const fields = controllers.map((controller) => {
+ const { callsign, frequency, logon_time } = controller;
+ const rating = vatsimData.ratings.find((rating) => rating.id === controller.rating);
+ const atis = vatsimData.atis.find((atis) => atis.cid === controller.cid);
- const totalCount = keys(vatsimControllers).length + keys(vatsimAtis).length;
- const shownCount = totalCount < 5 ? totalCount : 5;
+ return controllersListEmbedFields(callsign, frequency, handleLocaleDateTimeString(new Date(logon_time)), rating, atis);
+ }).splice(0, 5);
- return interaction.reply({ embeds: [listEmbed('Controllers & ATIS', fields, totalCount, shownCount, callsignSearch)] });
+ return interaction.editReply({ embeds: [listEmbed('Controllers & ATIS', fields.flat(), controllers.length, fields.length, callsignSearch)] });
}
diff --git a/src/commands/utils/vatsim/functions/vatsimEvents.ts b/src/commands/utils/vatsim/functions/vatsimEvents.ts
index 468c8f83..73b96dd8 100644
--- a/src/commands/utils/vatsim/functions/vatsimEvents.ts
+++ b/src/commands/utils/vatsim/functions/vatsimEvents.ts
@@ -1,5 +1,5 @@
import { ChatInputCommandInteraction, Colors, EmbedField } from 'discord.js';
-import { Logger, makeEmbed } from '../../../../lib';
+import { Logger, VatsimEventsSchema, fetchForeignAPI, makeEmbed } from '../../../../lib';
const BASE_VATSIM_URL = 'https://my.vatsim.net';
@@ -15,16 +15,13 @@ const handleLocaleDateString = (date: Date) => date.toLocaleDateString('en-US',
});
export async function handleVatsimEvents(interaction: ChatInputCommandInteraction<'cached'>) {
- await interaction.deferReply();
-
try {
- const eventsList = await fetch(`${BASE_VATSIM_URL}/api/v1/events/all`)
- .then((res) => res.json())
- .then((res) => res.data)
- .then((res) => res.filter((event: { type: string; }) => event.type === 'Event'))
- .then((res) => res.slice(0, 5));
+ const response = await fetchForeignAPI(`${BASE_VATSIM_URL}/api/v1/events/all`, VatsimEventsSchema);
+
+ const filteredEvents = response.data.filter((event) => event.type === 'Event');
+ const finalList = filteredEvents.slice(0, 5);
- const fields: EmbedField[] = eventsList.map((event: any) => {
+ const fields: EmbedField[] = finalList.map((event) => {
// eslint-disable-next-line camelcase
const { name, organisers, end_time, start_time, link } = event;
const { division } = organisers[0];
@@ -71,11 +68,11 @@ export async function handleVatsimEvents(interaction: ChatInputCommandInteractio
});
return interaction.editReply({ embeds: [eventsEmbed] });
- } catch (error: any) {
- Logger.error(error);
+ } catch (e) {
+ Logger.error(e);
const errorEmbed = makeEmbed({
title: 'Events Error',
- description: error.message,
+ description: String(e),
color: Colors.Red,
});
return interaction.editReply({ embeds: [errorEmbed] });
diff --git a/src/commands/utils/vatsim/functions/vatsimObservers.ts b/src/commands/utils/vatsim/functions/vatsimObservers.ts
index 48fd6ed1..9ad8cd8b 100644
--- a/src/commands/utils/vatsim/functions/vatsimObservers.ts
+++ b/src/commands/utils/vatsim/functions/vatsimObservers.ts
@@ -1,5 +1,8 @@
import { ChatInputCommandInteraction, EmbedField } from 'discord.js';
-import { makeEmbed } from '../../../../lib';
+import { z } from 'zod';
+import { VatsimRatingSchema, VatsimData, makeEmbed } from '../../../../lib';
+
+type Rating = z.infer;
/* eslint-disable camelcase */
@@ -9,7 +12,7 @@ const listEmbed = (type: string, fields: EmbedField[], totalCount: number, shown
fields,
});
-const observersListEmbedFields = (callsign: string, logon: string, rating: string, atis: string): EmbedField[] => {
+const observersListEmbedFields = (callsign: string, logon: string, rating?: Rating): EmbedField[] => {
const fields = [
{
name: 'Callsign',
@@ -21,18 +24,13 @@ const observersListEmbedFields = (callsign: string, logon: string, rating: strin
value: `${logon}`,
inline: true,
},
- {
- name: 'Rating',
- value: `${rating}`,
- inline: true,
- },
];
- if (atis !== null) {
- const atisTitle = 'Info';
+
+ if (rating) {
fields.push({
- name: atisTitle,
- value: atis,
- inline: false,
+ name: 'Rating',
+ value: `${rating.short} - ${rating.long}`,
+ inline: true,
});
}
@@ -47,28 +45,15 @@ const handleLocaleDateTimeString = (date: Date) => date.toLocaleDateString('en-U
day: 'numeric',
});
-export async function handleVatsimObservers(interaction: ChatInputCommandInteraction<'cached'>, vatsimData: any, callsignSearch: any) {
- const vatsimAllObservers = vatsimData.controllers ? vatsimData.controllers.filter((controller: { facility: number; }) => controller.facility <= 0) : null;
-
- const vatsimControllerRatings = vatsimData.ratings ? vatsimData.ratings : null;
- const vatsimObservers = vatsimAllObservers ? vatsimAllObservers.filter((observer: { callsign: string | any[]; }) => observer.callsign.includes(callsignSearch)) : null;
-
- const { keys }: ObjectConstructor = Object;
-
- const fields: EmbedField[] = [...vatsimObservers.sort((a: { rating: number; }, b: { rating: number; }) => b.rating - a.rating)].map((vatsimObserver) => {
- const { callsign, logon_time, text_atis, rating } = vatsimObserver;
- const logonTime = new Date(logon_time);
- const logonTimeString = handleLocaleDateTimeString(logonTime);
- const ratingDetail = vatsimControllerRatings.filter((ratingInfo: { id: any; }) => ratingInfo.id === rating);
- const { short, long } = ratingDetail[0];
- const ratingText = `${short} - ${long}`;
- const atisText = text_atis ? text_atis.join('\n') : null;
+export async function handleVatsimObservers(interaction: ChatInputCommandInteraction<'cached'>, vatsimData: VatsimData, callsignSearch: string) {
+ const observers = vatsimData.controllers.filter((controller) => controller.facility <= 0 && controller.callsign.includes(callsignSearch));
- return observersListEmbedFields(callsign, logonTimeString, ratingText, atisText);
- }).slice(0, 5).flat();
+ const fields = observers.map((observer) => {
+ const { callsign, logon_time } = observer;
+ const rating = vatsimData.ratings.find((rating) => rating.id === observer.rating);
- const totalCount = keys(vatsimObservers).length;
- const shownCount = totalCount < 5 ? totalCount : 5;
+ return observersListEmbedFields(callsign, handleLocaleDateTimeString(new Date(logon_time)), rating);
+ }).splice(0, 5);
- return interaction.reply({ embeds: [listEmbed('Observers', fields, totalCount, shownCount, callsignSearch)] });
+ return interaction.editReply({ embeds: [listEmbed('Observers', fields.flat(), observers.length, fields.length, callsignSearch)] });
}
diff --git a/src/commands/utils/vatsim/functions/vatsimPilots.ts b/src/commands/utils/vatsim/functions/vatsimPilots.ts
index e73c6300..70d9cf1f 100644
--- a/src/commands/utils/vatsim/functions/vatsimPilots.ts
+++ b/src/commands/utils/vatsim/functions/vatsimPilots.ts
@@ -1,5 +1,9 @@
import { ChatInputCommandInteraction, EmbedField } from 'discord.js';
-import { makeEmbed } from '../../../../lib';
+import { z } from 'zod';
+import { VatsimFlightPlanSchema, VatsimPilotRatingSchema, VatsimData, makeEmbed } from '../../../../lib';
+
+type PilotRating = z.infer;
+type FlightPlan = z.infer;
/* eslint-disable camelcase */
@@ -8,21 +12,24 @@ const listEmbed = (type: string, fields: EmbedField[], totalCount: number, shown
description: `A list of ${shownCount} online ${type} matching ${callsign}.`,
fields,
});
-const pilotsListEmbedFields = (callsign: string, rating: string, flightPlan: any) => {
+const pilotsListEmbedFields = (callsign: string, flightPlan: FlightPlan | null, rating?: PilotRating) => {
const fields = [
{
name: 'Callsign',
value: callsign,
inline: false,
},
- {
+ ];
+
+ if (rating) {
+ fields.push({
name: 'Rating',
- value: rating,
+ value: `${rating.short_name} - ${rating.long_name}`,
inline: true,
- },
- ];
+ });
+ }
- if (flightPlan !== null) {
+ if (flightPlan) {
const { aircraft_short, departure, arrival } = flightPlan;
fields.push(
{
@@ -41,23 +48,16 @@ const pilotsListEmbedFields = (callsign: string, rating: string, flightPlan: any
return fields;
};
-export async function handleVatsimPilots(interaction: ChatInputCommandInteraction<'cached'>, vatsimData: any, callsignSearch: any) {
- const vatsimPilotRatings = vatsimData.pilot_ratings ? vatsimData.pilot_ratings : null;
- const vatsimPilots = vatsimData.pilots ? vatsimData.pilots.filter((pilot: { callsign: (string | null)[]; }) => pilot.callsign.includes(callsignSearch)) : null;
-
- const { keys }: ObjectConstructor = Object;
-
- const fields: EmbedField[] = [...vatsimPilots.sort((a: { pilot_rating: number; }, b: { pilot_rating: number; }) => b.pilot_rating - a.pilot_rating)].map((vatsimPilot) => {
- const { callsign, pilot_rating, flight_plan } = vatsimPilot;
- const ratingDetail = vatsimPilotRatings.filter((ratingInfo: { id: number; }) => ratingInfo.id === pilot_rating);
- const { short_name, long_name } = ratingDetail[0];
- const ratingText = `${short_name} - ${long_name}`;
+export async function handleVatsimPilots(interaction: ChatInputCommandInteraction<'cached'>, vatsimData: VatsimData, callsignSearch: string) {
+ const pilots = vatsimData.pilots.filter((pilot) => pilot.callsign.includes(callsignSearch));
+ pilots.sort((a, b) => b.pilot_rating - a.pilot_rating);
- return pilotsListEmbedFields(callsign, ratingText, flight_plan);
- }).slice(0, 5).flat();
+ const fields = pilots.map((pilot) => {
+ const { callsign, flight_plan } = pilot;
+ const rating = vatsimData.pilot_ratings.find((rating) => rating.id === pilot.pilot_rating);
- const totalCount = keys(vatsimPilots).length;
- const shownCount = totalCount < 5 ? totalCount : 5;
+ return pilotsListEmbedFields(callsign, flight_plan, rating);
+ }).splice(0, 5);
- return interaction.reply({ embeds: [listEmbed('Pilots', fields, totalCount, shownCount, callsignSearch)] });
+ return interaction.editReply({ embeds: [listEmbed('Pilots', fields.flat(), pilots.length, fields.length, callsignSearch)] });
}
diff --git a/src/commands/utils/vatsim/functions/vatsimStats.ts b/src/commands/utils/vatsim/functions/vatsimStats.ts
index a37c7c82..dad5f63f 100644
--- a/src/commands/utils/vatsim/functions/vatsimStats.ts
+++ b/src/commands/utils/vatsim/functions/vatsimStats.ts
@@ -1,54 +1,47 @@
import { ChatInputCommandInteraction } from 'discord.js';
-import { makeEmbed } from '../../../../lib';
+import { VatsimData, makeEmbed } from '../../../../lib';
-const statsEmbed = (pilots: string, controllers: string, atis: string, observers: string, callsign: any) => makeEmbed({
+const statsEmbed = (pilots: number, controllers: number, atis: number, observers: number, callsign?: string) => makeEmbed({
title: callsign ? `VATSIM Data | Statistics for callsign ${callsign}` : 'VATSIM Data | Statistics',
description: callsign ? `An overview of the current active Pilots, Controllers, ATIS and Observers matching ${callsign}.` : 'An overview of the current active Pilots, Controllers, ATIS and Observers.',
fields: [
{
name: 'Pilots',
- value: pilots,
+ value: pilots.toString(),
inline: true,
},
{
name: 'Controllers',
- value: controllers,
+ value: controllers.toString(),
inline: true,
},
{
name: 'ATIS',
- value: atis,
+ value: atis.toString(),
inline: true,
},
{
name: 'Observers',
- value: observers,
+ value: observers.toString(),
inline: true,
},
],
});
-export async function handleVatsimStats(interaction: ChatInputCommandInteraction<'cached'>, vatsimData: any, callsignSearch: any) {
- const vatsimAllControllers = vatsimData.controllers ? vatsimData.controllers.filter((controller: { facility: number; }) => controller.facility > 0) : null;
- const vatsimAllObservers = vatsimData.controllers ? vatsimData.controllers.filter((controller: { facility: number; }) => controller.facility <= 0) : null;
+export async function handleVatsimStats(interaction: ChatInputCommandInteraction<'cached'>, vatsimData: VatsimData, callsignSearch?: string) {
+ const controllers = vatsimData.controllers.filter((controller) => controller.facility > 0);
+ const observers = vatsimData.controllers.filter((controller) => controller.facility <= 0);
+ const { atis } = vatsimData;
+ const { pilots } = vatsimData;
if (!callsignSearch) {
- const vatsimPilotCount = vatsimData.pilots ? vatsimData.pilots.length : 0;
- const vatsimControllerCount = vatsimAllControllers ? vatsimAllControllers.length : 0;
- const vatsimAtisCount = vatsimData.atis ? vatsimData.atis.length : 0;
- const vatsimObserverCount = vatsimAllObservers ? vatsimAllObservers.length : 0;
-
- return interaction.reply({ embeds: [statsEmbed(vatsimPilotCount, vatsimControllerCount, vatsimAtisCount, vatsimObserverCount, null)] });
+ return interaction.editReply({ embeds: [statsEmbed(pilots.length, controllers.length, atis.length, observers.length)] });
}
- const vatsimPilots = vatsimData.pilots ? vatsimData.pilots.filter((pilot: { callsign: string | string[]; }) => pilot.callsign.includes(callsignSearch)) : null;
- const vatsimControllers = vatsimAllControllers ? vatsimAllControllers.filter((controller: { callsign: string | string[]; }) => controller.callsign.includes(callsignSearch)) : null;
- const vatsimAtis = vatsimData.atis ? vatsimData.atis.filter((atis: { callsign: string | string[]; }) => atis.callsign.includes(callsignSearch)) : null;
- const vatsimObservers = vatsimAllObservers ? vatsimAllObservers.filter((observer: { callsign: string | string[]; }) => observer.callsign.includes(callsignSearch)) : null;
- const vatsimPilotCount = vatsimPilots ? vatsimPilots.length : 0;
- const vatsimControllerCount = vatsimControllers ? vatsimControllers.length : 0;
- const vatsimAtisCount = vatsimAtis ? vatsimAtis.length : 0;
- const vatsimObserverCount = vatsimObservers ? vatsimObservers.length : 0;
+ const matchedControllers = controllers.filter((controller) => controller.callsign.includes(callsignSearch));
+ const matchedObservers = observers.filter((observer) => observer.callsign.includes(callsignSearch));
+ const matchedPilots = pilots.filter((pilot) => pilot.callsign.includes(callsignSearch));
+ const matchedAtis = atis.filter((atis) => atis.callsign.includes(callsignSearch));
- return interaction.reply({ embeds: [statsEmbed(vatsimPilotCount, vatsimControllerCount, vatsimAtisCount, vatsimObserverCount, callsignSearch)] });
+ return interaction.editReply({ embeds: [statsEmbed(matchedPilots.length, matchedControllers.length, matchedAtis.length, matchedObservers.length, callsignSearch)] });
}
diff --git a/src/commands/utils/vatsim/vatsim.ts b/src/commands/utils/vatsim/vatsim.ts
index c1511937..d3dfbf66 100644
--- a/src/commands/utils/vatsim/vatsim.ts
+++ b/src/commands/utils/vatsim/vatsim.ts
@@ -1,10 +1,11 @@
import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js';
-import { makeEmbed, slashCommand, slashCommandStructure } from '../../../lib';
-import { handleVatsimStats } from './functions/vatsimStats';
+import { ZodError } from 'zod';
+import { Logger, VatsimData, VatsimDataSchema, fetchForeignAPI, makeEmbed, slashCommand, slashCommandStructure } from '../../../lib';
import { handleVatsimControllers } from './functions/vatsimControllers';
-import { handleVatsimPilots } from './functions/vatsimPilots';
-import { handleVatsimObservers } from './functions/vatsimObservers';
import { handleVatsimEvents } from './functions/vatsimEvents';
+import { handleVatsimObservers } from './functions/vatsimObservers';
+import { handleVatsimPilots } from './functions/vatsimPilots';
+import { handleVatsimStats } from './functions/vatsimStats';
const data = slashCommandStructure({
name: 'vatsim',
@@ -85,25 +86,23 @@ const fetchErrorEmbed = (error: any) => makeEmbed({
});
export default slashCommand(data, async ({ interaction }) => {
- // Fetch VATSIM data
+ await interaction.deferReply();
- let vatsimData;
+ // Fetch VATSIM data
+ let vatsimData: VatsimData;
try {
- vatsimData = await fetch('https://data.vatsim.net/v3/vatsim-data.json').then((response) => {
- if (response.ok) {
- return response.json();
- }
- throw new Error(response.statusText);
- });
- } catch (error) {
- await interaction.reply({ embeds: [fetchErrorEmbed(error)], ephemeral: true });
- return;
+ vatsimData = await fetchForeignAPI('https://data.vatsim.net/v3/vatsim-data.json', VatsimDataSchema);
+ } catch (e) {
+ if (e instanceof ZodError) {
+ return interaction.editReply({ embeds: [fetchErrorEmbed('The VATSIM API returned unknown data.')] });
+ }
+ Logger.error(`Error while fetching VATSIM data: ${String(e)}.`);
+ return interaction.editReply({ embeds: [fetchErrorEmbed('An error occurred while fetching data from VATSIM.')] });
}
// Grap the callsign from the interaction
-
let callsign = interaction.options.getString('callsign');
- let callsignSearch;
+ let callsignSearch: string | undefined;
if (callsign) {
callsign = callsign.toUpperCase();
@@ -112,35 +111,42 @@ export default slashCommand(data, async ({ interaction }) => {
const regexMatches = callsign.match(regexCheck);
if (!regexMatches || !regexMatches.groups || !regexMatches.groups.callsignSearch) {
- // eslint-disable-next-line consistent-return
- return interaction.reply({ content: 'You need to provide a valid callsign or part of a callsign to search for', ephemeral: true });
+ return interaction.editReply({ content: 'You need to provide a valid callsign or part of a callsign to search for.' });
}
callsignSearch = regexMatches.groups.callsignSearch;
}
// Handle the subcommands
-
const subcommandName = interaction.options.getSubcommand();
switch (subcommandName) {
- case 'stats':
- await handleVatsimStats(interaction, vatsimData, callsignSearch);
- break;
- case 'controllers':
- await handleVatsimControllers(interaction, vatsimData, callsignSearch);
- break;
- case 'pilots':
- await handleVatsimPilots(interaction, vatsimData, callsignSearch);
- break;
- case 'observers':
- await handleVatsimObservers(interaction, vatsimData, callsignSearch);
- break;
- case 'events':
- await handleVatsimEvents(interaction);
- break;
-
- default:
- await interaction.reply({ content: 'Unknown subcommand', ephemeral: true });
+ case 'stats': {
+ return handleVatsimStats(interaction, vatsimData, callsignSearch);
+ }
+ case 'controllers': {
+ if (!callsignSearch) {
+ return interaction.editReply({ content: 'You need to provide a valid callsign or part of a callsign to search for.' });
+ }
+ return handleVatsimControllers(interaction, vatsimData, callsignSearch);
+ }
+ case 'pilots': {
+ if (!callsignSearch) {
+ return interaction.editReply({ content: 'You need to provide a valid callsign or part of a callsign to search for.' });
+ }
+ return handleVatsimPilots(interaction, vatsimData, callsignSearch);
+ }
+ case 'observers': {
+ if (!callsignSearch) {
+ return interaction.editReply({ content: 'You need to provide a valid callsign or part of a callsign to search for.' });
+ }
+ return handleVatsimObservers(interaction, vatsimData, callsignSearch);
+ }
+ case 'events': {
+ return handleVatsimEvents(interaction);
+ }
+ default: {
+ return interaction.editReply({ content: 'Unknown subcommand' });
+ }
}
});
diff --git a/src/commands/utils/wolframAlpha.ts b/src/commands/utils/wolframAlpha.ts
index 71ed2744..c0dbc686 100644
--- a/src/commands/utils/wolframAlpha.ts
+++ b/src/commands/utils/wolframAlpha.ts
@@ -1,5 +1,9 @@
import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js';
-import { slashCommand, slashCommandStructure, makeEmbed, makeLines, Logger } from '../../lib';
+import { z, ZodError } from 'zod';
+import { fetchForeignAPI, Logger, makeEmbed, makeLines, slashCommand, slashCommandStructure, WolframAlphaData, WolframAlphaDataSchema, WolframAlphaPodSchema, WolframAlphaSubpodSchema } from '../../lib';
+
+type Pod = z.infer;
+type Subpod = z.infer;
const data = slashCommandStructure({
name: 'wolframalpha',
@@ -19,6 +23,12 @@ const noQueryEmbed = makeEmbed({
color: Colors.Red,
});
+const errorEmbed = (error: string) => makeEmbed({
+ title: 'Wolfram Alpha Error',
+ description: error,
+ color: Colors.Red,
+});
+
const WOLFRAMALPHA_API_URL = 'https://api.wolframalpha.com/v2/query?';
const WOLFRAMALPHA_QUERY_URL = 'https://www.wolframalpha.com/input/?';
@@ -33,12 +43,12 @@ export default slashCommand(data, async ({ interaction }) => {
description: 'Wolfram Alpha token not found.',
color: Colors.Red,
});
- return interaction.followUp({ embeds: [noTokenEmbed], ephemeral: true });
+ return interaction.editReply({ embeds: [noTokenEmbed] });
}
const query = interaction.options.getString('query');
- if (!query) return interaction.followUp({ embeds: [noQueryEmbed], ephemeral: true });
+ if (!query) return interaction.editReply({ embeds: [noQueryEmbed] });
const params = {
appid: wolframAlphaToken,
@@ -49,72 +59,62 @@ export default slashCommand(data, async ({ interaction }) => {
const searchParams = new URLSearchParams(params);
+ let response: WolframAlphaData;
try {
- const response = await fetch(`${WOLFRAMALPHA_API_URL}${searchParams.toString()}`)
- .then((res) => res.json());
-
- if (response.error) {
- const errorEmbed = makeEmbed({
- title: 'Wolfram Alpha Error',
- description: response.error,
- color: Colors.Red,
- });
- return interaction.followUp({ embeds: [errorEmbed], ephemeral: true });
+ response = await fetchForeignAPI(`${WOLFRAMALPHA_API_URL}${searchParams.toString()}`, WolframAlphaDataSchema);
+ } catch (e) {
+ if (e instanceof ZodError) {
+ return interaction.editReply({ embeds: [errorEmbed('Wolfram Alpha returned unknown data.')] });
}
+ Logger.error(`Error while fetching from Wolfram Alpha: ${String(e)}`);
+ return interaction.editReply({ embeds: [errorEmbed('An error occurred while fetching from Wolfram Alpha.')] });
+ }
- if (response.queryresult.success === true) {
- const podTexts: string[] = [];
- response.queryresult.pods.forEach((pod: any) => {
- if (pod.id !== 'Input' && pod.primary === true) {
- const results: string[] = [];
- pod.subpods.forEach((subpod: any) => {
- results.push(subpod.plaintext);
- });
- if (results.length > 0) {
- podTexts.push(`**${pod.title}:** \n${results.join('\n')}`);
- }
- }
- });
- if (podTexts.length > 0) {
- const result = podTexts.join('\n\n');
- const queryParams = new URLSearchParams({ i: query });
-
- const waEmbed = makeEmbed({
- description: makeLines([
- `**Query:** ${query}`,
- '',
- result,
- '',
- `[Web Result](${WOLFRAMALPHA_QUERY_URL}${queryParams.toString()})`,
- ]),
+ if (response.queryresult.success === true) {
+ const podTexts: string[] = [];
+ response.queryresult.pods.forEach((pod: Pod) => {
+ if (pod.id !== 'Input' && pod.primary === true) {
+ const results: string[] = [];
+ pod.subpods.forEach((subpod: Subpod) => {
+ results.push(subpod.plaintext);
});
-
- return interaction.followUp({ embeds: [waEmbed] });
+ if (results.length > 0) {
+ podTexts.push(`**${pod.title}:** \n${results.join('\n')}`);
+ }
}
- const noResultsEmbed = makeEmbed({
- title: 'Wolfram Alpha Error | No Results',
+ });
+ if (podTexts.length > 0) {
+ const result = podTexts.join('\n\n');
+ const queryParams = new URLSearchParams({ i: query });
+
+ const waEmbed = makeEmbed({
description: makeLines([
- 'No results were found for your query.',
+ `**Query:** ${query}`,
+ '',
+ result,
+ '',
+ `[Web Result](${WOLFRAMALPHA_QUERY_URL}${queryParams.toString()})`,
]),
- color: Colors.Red,
});
- return interaction.followUp({ embeds: [noResultsEmbed], ephemeral: true });
+
+ return interaction.editReply({ embeds: [waEmbed] });
}
- const obscureQueryEmbed = makeEmbed({
- title: 'Wolfram Alpha Error | Could not understand query',
+ const noResultsEmbed = makeEmbed({
+ title: 'Wolfram Alpha Error | No Results',
description: makeLines([
- 'Wolfram Alpha could not understand your query.',
+ 'No results were found for your query.',
]),
color: Colors.Red,
});
- return interaction.followUp({ embeds: [obscureQueryEmbed], ephemeral: true });
- } catch (e) {
- Logger.error('wolframalpha:', e);
- const fetchErrorEmbed = makeEmbed({
- title: 'Wolfram Alpha | Fetch Error',
- description: 'There was an error fetching your request. Please try again later.',
- color: Colors.Red,
- });
- return interaction.followUp({ embeds: [fetchErrorEmbed], ephemeral: true });
+ return interaction.editReply({ embeds: [noResultsEmbed] });
}
+
+ const obscureQueryEmbed = makeEmbed({
+ title: 'Wolfram Alpha Error | Could not understand query',
+ description: makeLines([
+ 'Wolfram Alpha could not understand your query.',
+ ]),
+ color: Colors.Red,
+ });
+ return interaction.editReply({ embeds: [obscureQueryEmbed] });
});
diff --git a/src/lib/apis/fetchForeignAPI.ts b/src/lib/apis/fetchForeignAPI.ts
new file mode 100644
index 00000000..f6625cf3
--- /dev/null
+++ b/src/lib/apis/fetchForeignAPI.ts
@@ -0,0 +1,46 @@
+import fetch, { Request, RequestInfo, Response } from 'node-fetch';
+import { ZodSchema } from 'zod';
+import { Logger } from '../logger';
+
+/**
+ * Fetch data from any API endpoint that returns JSON formatted data.
+ * @typeParam ReturnType - The expected type of the returned data.
+ * @param request The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object to be passed to `fetch()`.
+ * @param zodSchema The [Zod](https://github.com/colinhacks/zod) schema that the returned data conforms to. The promise will reject if the returned data does not conform to the schema provided.
+ * @returns A promise that resolves to the expected type or rejects with an [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) or a {@link ZodError} if the validation failed.
+ */
+export const fetchForeignAPI = async (request: RequestInfo, zodSchema: ZodSchema, debug?: boolean): Promise => {
+ const req = new Request(request);
+
+ let response: Response;
+ try {
+ response = await fetch(req);
+ } catch (e) {
+ throw new Error(`An error occured while fetching data from ${req.url}: ${String(e)}`);
+ }
+
+ if (!response.ok) {
+ throw new Error(`HTTP Error. Status: ${response.status}`);
+ }
+
+ let data: unknown;
+ try {
+ data = await response.json();
+ } catch (e) {
+ throw new Error(`Could not parse JSON. Make sure the endpoint at ${req.url} returns valid JSON. Error: ${String(e)}`);
+ }
+
+ const result = zodSchema.safeParse(data);
+
+ if (!result.success) {
+ Logger.error("[zod] Data validation failed! Pass the 'debug' flag to 'fetchForeignAPI()' to dump the retrieved data to the console.");
+ Logger.error(`Endpoint location: ${req.url}.`);
+ if (debug) {
+ Logger.debug('RETRIEVED DATA:', data);
+ }
+ result.error.issues.forEach((issue) => Logger.error(`[zod] Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`));
+ throw result.error;
+ }
+
+ return result.data;
+};
diff --git a/src/lib/apis/zodSchemas/avwx/metarSchemas.ts b/src/lib/apis/zodSchemas/avwx/metarSchemas.ts
new file mode 100644
index 00000000..fcf39046
--- /dev/null
+++ b/src/lib/apis/zodSchemas/avwx/metarSchemas.ts
@@ -0,0 +1,46 @@
+import { z } from 'zod';
+
+export const MetarTimeSchema = z.object({ dt: z.string().datetime() });
+
+export const MetarWindDirectionSchema = z.object({ repr: z.string() });
+
+export const MetarWindSpeedSchema = z.object({ repr: z.string() });
+
+export const MetarVisibilitySchema = z.object({ repr: z.string() });
+
+export const MetarTemperatureSchema = z.object({ repr: z.string() });
+
+export const MetarDewpointSchema = z.object({ repr: z.string() });
+
+export const MetarAltimeterSchema = z.object({ value: z.number() });
+
+export const MetarUnitsSchema = z.object({
+ accumulation: z.string(),
+ altimeter: z.string(),
+ altitude: z.string(),
+ temperature: z.string(),
+ visibility: z.string(),
+ wind_speed: z.string(),
+});
+
+/**
+ * This schema only contains currently used fields. If you wish to use other fields returned by the API add them in this file.
+ */
+export const MetarSchema = z.object({
+ station: z.string(),
+ raw: z.string(),
+ time: MetarTimeSchema,
+ wind_direction: MetarWindDirectionSchema,
+ wind_speed: MetarWindSpeedSchema,
+ visibility: MetarVisibilitySchema,
+ temperature: MetarTemperatureSchema,
+ dewpoint: MetarDewpointSchema,
+ altimeter: MetarAltimeterSchema,
+ flight_rules: z.string(),
+ units: MetarUnitsSchema,
+});
+
+/**
+ * This type only contains currently used fields. If you wish to use other fields returned by the API add them in this file.
+ */
+export type Metar = z.infer;
diff --git a/src/lib/apis/zodSchemas/avwx/stationSchemas.ts b/src/lib/apis/zodSchemas/avwx/stationSchemas.ts
new file mode 100644
index 00000000..7f08f268
--- /dev/null
+++ b/src/lib/apis/zodSchemas/avwx/stationSchemas.ts
@@ -0,0 +1,25 @@
+import { z } from 'zod';
+
+export const AVWXRunwaySchema = z.object({
+ length_ft: z.number(),
+ width_ft: z.number(),
+ ident1: z.string(),
+ ident2: z.string(),
+});
+
+export const AVWXStationSchema = z.object({
+ city: z.string(),
+ country: z.string(),
+ elevation_ft: z.number(),
+ elevation_m: z.number(),
+ icao: z.string(),
+ latitude: z.number(),
+ longitude: z.number(),
+ name: z.string(),
+ runways: z.nullable(z.array(AVWXRunwaySchema)),
+ type: z.string(),
+ website: z.nullable(z.string().url()),
+ wiki: z.nullable(z.string().url()),
+});
+
+export type AVWXStation = z.infer;
diff --git a/src/lib/apis/zodSchemas/avwx/tafSchemas.ts b/src/lib/apis/zodSchemas/avwx/tafSchemas.ts
new file mode 100644
index 00000000..69356b74
--- /dev/null
+++ b/src/lib/apis/zodSchemas/avwx/tafSchemas.ts
@@ -0,0 +1,16 @@
+import { z } from 'zod';
+
+export const TafForecastSchema = z.object({ raw: z.string() });
+
+/**
+ * This schema only contains currently used fields. If you wish to use other fields returned by the API add them in this file.
+ */
+export const TafSchema = z.object({
+ raw: z.string(),
+ station: z.string(),
+ forecast: z.array(TafForecastSchema),
+});
+/**
+ * This type only contains currently used fields. If you wish to use other fields returned by the API add them in this file.
+ */
+export type TAF = z.infer;
diff --git a/src/lib/apis/zodSchemas/flybywire/telex.ts b/src/lib/apis/zodSchemas/flybywire/telex.ts
new file mode 100644
index 00000000..8a670976
--- /dev/null
+++ b/src/lib/apis/zodSchemas/flybywire/telex.ts
@@ -0,0 +1,5 @@
+import { z } from 'zod';
+
+export const TelexCountSchema = z.number();
+
+export type TelexCount = z.infer;
diff --git a/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts b/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts
new file mode 100644
index 00000000..3b1b77ea
--- /dev/null
+++ b/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts
@@ -0,0 +1,43 @@
+import { z } from 'zod';
+
+const SimBriefFetchSchema = z.object({ status: z.string() });
+
+const SimBriefParamsSchema = z.object({
+ user_id: z.string(),
+ time_generated: z.string(),
+ airac: z.string(),
+});
+
+const SimBriefAircraftSchema = z.object({
+ name: z.string(),
+ internal_id: z.string(),
+});
+
+const SimBriefOriginSchema = z.object({
+ icao_code: z.string(),
+ plan_rwy: z.string(),
+});
+
+const SimBriefDestinationSchema = z.object({
+ icao_code: z.string(),
+ plan_rwy: z.string(),
+});
+
+const SimBriefGeneralSchema = z.object({ route: z.string() });
+
+/**
+ * This schema only contains currently used fields. If you wish to use other fields returned by the API add them in this file.
+ */
+export const SimbriefFlightPlanSchema = z.object({
+ fetch: SimBriefFetchSchema,
+ params: SimBriefParamsSchema,
+ aircraft: SimBriefAircraftSchema,
+ origin: SimBriefOriginSchema,
+ destination: SimBriefDestinationSchema,
+ general: SimBriefGeneralSchema,
+});
+
+/**
+ * This type only contains currently used fields. If you wish to use other fields returned by the API add them in this file.
+ */
+export type SimbriefFlightPlan = z.infer;
diff --git a/src/lib/apis/zodSchemas/vatsim/vatsimDataSchemas.ts b/src/lib/apis/zodSchemas/vatsim/vatsimDataSchemas.ts
new file mode 100644
index 00000000..aaed9092
--- /dev/null
+++ b/src/lib/apis/zodSchemas/vatsim/vatsimDataSchemas.ts
@@ -0,0 +1,146 @@
+import { z } from 'zod';
+
+export const VatsimMilitaryRatingSchema = z.object({
+ id: z.number(),
+ short_name: z.string(),
+ long_name: z.string(),
+});
+
+export const VatsimPilotRatingSchema = z.object({
+ id: z.number(),
+ short_name: z.string(),
+ long_name: z.string(),
+});
+
+export const VatsimRatingSchema = z.object({
+ id: z.number(),
+ short: z.string(),
+ long: z.string(),
+});
+
+export const VatsimFacilitySchema = z.object({
+ id: z.number(),
+ short: z.string(),
+ long: z.string(),
+});
+
+export const VatsimFlightPlanSchema = z.object({
+ flight_rules: z.enum(['I', 'V']),
+ aircraft: z.string(),
+ aircraft_faa: z.string(),
+ aircraft_short: z.string(),
+ departure: z.string(),
+ arrival: z.string(),
+ alternate: z.string(),
+ deptime: z.string(),
+ enroute_time: z.string(),
+ fuel_time: z.string(),
+ remarks: z.string(),
+ route: z.string(),
+ revision_id: z.number(),
+ assigned_transponder: z.string(),
+});
+
+export const VatsimPrefileSchema = z.object({
+ cid: z.number(),
+ name: z.string(),
+ callsign: z.string(),
+ flight_plan: VatsimFlightPlanSchema,
+ last_updated: z.string(),
+});
+
+export const VatsimServerSchema = z.object({
+ ident: z.string(),
+ hostname_or_ip: z.string(),
+ location: z.string(),
+ name: z.string(),
+ /**
+ * @deprecated
+ */
+ clients_connection_allowed: z.number(),
+ client_connections_allowed: z.boolean(),
+ is_sweatbox: z.boolean(),
+});
+
+export const VatsimAtisSchema = z.object({
+ cid: z.number(),
+ name: z.string(),
+ callsign: z.string(),
+ frequency: z.string(),
+ facility: z.number(),
+ rating: z.number(),
+ server: z.string(),
+ visual_range: z.number(),
+ atis_code: z.nullable(z.string()),
+ text_atis: z.nullable(z.array(z.string())),
+ last_updated: z.string(),
+ logon_time: z.string(),
+});
+
+export const VatsimControllerSchema = z.object({
+ cid: z.number(),
+ name: z.string(),
+ callsign: z.string(),
+ facility: z.number(),
+ frequency: z.string(),
+ rating: z.number(),
+ server: z.string(),
+ visual_range: z.number(),
+ text_atis: z.nullable(z.array(z.string())),
+ last_updated: z.string(),
+ logon_time: z.string(),
+});
+
+export const VatsimPilotSchema = z.object({
+ cid: z.number(),
+ name: z.string(),
+ callsign: z.string(),
+ server: z.string(),
+ pilot_rating: z.number(),
+ military_rating: z.number(),
+ latitude: z.number(),
+ longitude: z.number(),
+ altitude: z.number(),
+ groundspeed: z.number(),
+ transponder: z.string(),
+ heading: z.number(),
+ qnh_i_hg: z.number(),
+ qnh_mb: z.number(),
+ flight_plan: z.nullable(VatsimFlightPlanSchema),
+ logon_time: z.string(),
+ last_updated: z.string(),
+});
+
+export const VatsimGeneralSchema = z.object({
+ version: z.number(),
+ /**
+ * @deprecated
+ */
+ reload: z.number(),
+ /**
+ * @deprecated
+ */
+ update: z.string(),
+ update_timestamp: z.string(),
+ connected_clients: z.number(),
+ unique_users: z.number(),
+});
+
+/**
+ * Note: The docs do not completely align with actual returned data. The schemas reflect actual returned data structures.
+ * @see https://vatsim.dev/api/data-api/get-network-data
+ */
+export const VatsimDataSchema = z.object({
+ general: VatsimGeneralSchema,
+ pilots: z.array(VatsimPilotSchema),
+ controllers: z.array(VatsimControllerSchema),
+ atis: z.array(VatsimAtisSchema),
+ servers: z.array(VatsimServerSchema),
+ prefiles: z.array(VatsimPrefileSchema),
+ facilities: z.array(VatsimFacilitySchema),
+ ratings: z.array(VatsimRatingSchema),
+ pilot_ratings: z.array(VatsimPilotRatingSchema),
+ military_ratings: z.array(VatsimMilitaryRatingSchema),
+});
+
+export type VatsimData = z.infer;
diff --git a/src/lib/apis/zodSchemas/vatsim/vatsimEventsSchemas.ts b/src/lib/apis/zodSchemas/vatsim/vatsimEventsSchemas.ts
new file mode 100644
index 00000000..d4122445
--- /dev/null
+++ b/src/lib/apis/zodSchemas/vatsim/vatsimEventsSchemas.ts
@@ -0,0 +1,38 @@
+import { z } from 'zod';
+
+export const VatsimOrganiserSchema = z.object({
+ region: z.nullable(z.string()),
+ division: z.nullable(z.string()),
+ subdivision: z.nullable(z.string()),
+ organised_by_vatsim: z.boolean(),
+});
+
+export const VatsimAirportSchema = z.object({ icao: z.string() });
+
+export const VatsimRouteSchema = z.object({
+ departure: z.string(),
+ arrival: z.string(),
+ route: z.string(),
+});
+
+export const VatsimEventsDataSchema = z.object({
+ id: z.number(),
+ type: z.enum(['Event', 'Controller Examination', 'VASOPS Event']),
+ name: z.string(),
+ link: z.string(),
+ organisers: z.array(VatsimOrganiserSchema),
+ airports: z.array(VatsimAirportSchema),
+ routes: z.array(VatsimRouteSchema),
+ start_time: z.string(),
+ end_time: z.string(),
+ short_description: z.string(),
+ description: z.string(),
+ banner: z.string(),
+});
+
+/**
+ * @see https://vatsim.dev/api/events-api/1.0.0/list-events
+ */
+export const VatsimEventsSchema = z.object({ data: z.array(VatsimEventsDataSchema) });
+
+export type VatsimEvents = z.infer;
diff --git a/src/lib/apis/zodSchemas/wolframAlpha/wolframAlphaSchemas.ts b/src/lib/apis/zodSchemas/wolframAlpha/wolframAlphaSchemas.ts
new file mode 100644
index 00000000..07d937bb
--- /dev/null
+++ b/src/lib/apis/zodSchemas/wolframAlpha/wolframAlphaSchemas.ts
@@ -0,0 +1,50 @@
+import { z } from 'zod';
+
+export const WolframAlphaErrorSchema = z.object({
+ code: z.number(),
+ msg: z.string(),
+});
+
+export const WolframAlphaSubpodSchema = z.object({
+ title: z.string(),
+ plaintext: z.string({ message: 'Only appears if the requested result formats include plain text.' }),
+});
+
+export const WolframAlphaPodSchema = z.object({
+ title: z.string(),
+ error: z.union([z.boolean(), WolframAlphaErrorSchema]),
+ position: z.number(),
+ scanner: z.string(),
+ id: z.string(),
+ numsubpods: z.number(),
+ primary: z.optional(z.boolean()),
+ subpods: z.array(WolframAlphaSubpodSchema),
+});
+
+const BaseQueryResultSchema = z.object({
+ error: z.union([z.boolean(), WolframAlphaErrorSchema]),
+ numpods: z.number(),
+ version: z.string(),
+ datatypes: z.string(),
+ timing: z.number(),
+ timedout: z.union([z.string(), z.number()]),
+ parsetiming: z.number(),
+ parsetimedout: z.boolean(),
+ recalculate: z.string(),
+});
+
+const SuccessQueryResultSchema = BaseQueryResultSchema.extend({
+ success: z.literal(true),
+ pods: z.array(WolframAlphaPodSchema),
+});
+
+const NoSuccessQueryResultSchema = BaseQueryResultSchema.extend({ success: z.literal(false) });
+
+export const WolframAlphaQueryResultSchema = z.discriminatedUnion('success', [SuccessQueryResultSchema, NoSuccessQueryResultSchema]);
+
+export const WolframAlphaDataSchema = z.object({ queryresult: WolframAlphaQueryResultSchema });
+
+/**
+ * This type only includes currently used properties. If you wish to extend its functionality, add the relevant schemas in this file.
+ */
+export type WolframAlphaData = z.infer;
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 2f98d658..4bb22deb 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -26,3 +26,14 @@ export * from './schedulerJobs/refreshInMemoryCache';
//Cache Management
export * from './cache/cacheManager';
+
+// API Wrapper
+export * from './apis/fetchForeignAPI';
+export * from './apis/zodSchemas/vatsim/vatsimEventsSchemas';
+export * from './apis/zodSchemas/vatsim/vatsimDataSchemas';
+export * from './apis/zodSchemas/avwx/metarSchemas';
+export * from './apis/zodSchemas/avwx/tafSchemas';
+export * from './apis/zodSchemas/avwx/stationSchemas';
+export * from './apis/zodSchemas/simbrief/simbriefSchemas';
+export * from './apis/zodSchemas/wolframAlpha/wolframAlphaSchemas';
+export * from './apis/zodSchemas/flybywire/telex';