Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generic wrapper for JSON APIs #70

Draft
wants to merge 36 commits into
base: staging
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
759758a
add fetchData
ExampleWasTaken Jun 25, 2024
c15b315
add vatsim-event endpoint types
ExampleWasTaken Jun 25, 2024
211579d
add util type guards
ExampleWasTaken Jun 25, 2024
684ca62
migrate vatsim events to the new api wrapper
ExampleWasTaken Jun 25, 2024
7ab2383
add vatsim data endpoint types
ExampleWasTaken Jun 25, 2024
bd67559
add zod library
ExampleWasTaken Jul 1, 2024
bd0b412
partial vatsim port to zod
ExampleWasTaken Jul 1, 2024
0f7ec61
migrate vatsim observer to zod
ExampleWasTaken Jul 2, 2024
992c700
add missing flight plan type export
ExampleWasTaken Jul 2, 2024
81b9045
migrate vatsim pilot to zod
ExampleWasTaken Jul 2, 2024
7b37d6e
migrate vatsim stats to zod
ExampleWasTaken Jul 2, 2024
7062fc0
fix lint in vatsim
ExampleWasTaken Jul 2, 2024
5e2a3b7
migrate vatsimEvents
ExampleWasTaken Jul 2, 2024
33e3219
improve var naming
ExampleWasTaken Jul 2, 2024
7a0c89b
refactor fetchData
ExampleWasTaken Jul 2, 2024
31c2ea3
remove any ewww
ExampleWasTaken Jul 2, 2024
540557a
migrate taf cmd to zod
ExampleWasTaken Jul 3, 2024
0bffea0
migrate metar cmd to zod
ExampleWasTaken Jul 3, 2024
5c1299b
add disclaimer comments to taf and metar schemas
ExampleWasTaken Jul 3, 2024
f24dd5c
migrate simbrief cmd to zod
ExampleWasTaken Jul 3, 2024
50d08dc
migrate wolfram alpha cmd to zod
ExampleWasTaken Jul 3, 2024
0044d3c
fix zod error being thrown incorrectly while fetching data
ExampleWasTaken Jul 3, 2024
d1a8704
add folder structure to zod schema directory
ExampleWasTaken Jul 3, 2024
c841fea
migrate station cmd to zod
ExampleWasTaken Jul 4, 2024
698074e
migrate live-flight cmd to zod
ExampleWasTaken Jul 4, 2024
eae9c22
rename fetchData to fetchForeignAPI
ExampleWasTaken Jul 4, 2024
03adb0d
improve fetchForeignAPI zod error logging
ExampleWasTaken Jul 4, 2024
c01ad7a
allow string and url when fetching from foreign apis
ExampleWasTaken Jul 4, 2024
6647032
use string instead of request object for foreign apis
ExampleWasTaken Jul 4, 2024
0b2de9e
add changelog
ExampleWasTaken Jul 28, 2024
a5b363e
self review
ExampleWasTaken Jul 4, 2024
aa84fa9
update simbrief zod schema
ExampleWasTaken Aug 8, 2024
7e46e6b
remove console
ExampleWasTaken Aug 26, 2024
e4eca0d
Merge branch 'staging' into feat/generic-api-wrapper
ExampleWasTaken Oct 27, 2024
256b6db
update changelog
ExampleWasTaken Oct 27, 2024
777c5b6
Merge branch 'staging' into feat/generic-api-wrapper
ExampleWasTaken Nov 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
## Changelog

Update <small>_ November 2024</small>

- feat: generic wrapper for JSON APIs (01/11/2024)

Update <small>_ October 2024</small>

- 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 <small>_ August 2024</small>
Expand Down
11 changes: 10 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
19 changes: 15 additions & 4 deletions src/commands/utils/liveFlights.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -11,24 +12,34 @@ 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.`,
footer: { text: 'Note: This includes the A32NX, and other aircraft using FlyByWire systems' },
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({
title: 'Error | Live Flights',
description: error.message,
color: Colors.Red,
});
return interaction.reply({ embeds: [errorEmbed] });
return interaction.editReply({ embeds: [errorEmbed] });
}
});
93 changes: 45 additions & 48 deletions src/commands/utils/metar.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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();

Expand All @@ -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] });
});
37 changes: 24 additions & 13 deletions src/commands/utils/simbriefData.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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,
});

Expand All @@ -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}`,
Expand All @@ -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<SimbriefFlightPlan>(`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.' });
});
Loading
Loading