From 759758a125f6408f6a3a66fc77a22f9cd9e2a269 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 25 Jun 2024 07:26:51 +0200 Subject: [PATCH 01/34] add fetchData --- src/lib/apiWrapper/fetchData.ts | 35 +++++++++++++++++++++++++++++++++ src/lib/apiWrapper/typeGuard.ts | 1 + src/lib/index.ts | 4 ++++ 3 files changed, 40 insertions(+) create mode 100644 src/lib/apiWrapper/fetchData.ts create mode 100644 src/lib/apiWrapper/typeGuard.ts diff --git a/src/lib/apiWrapper/fetchData.ts b/src/lib/apiWrapper/fetchData.ts new file mode 100644 index 00000000..1c840e05 --- /dev/null +++ b/src/lib/apiWrapper/fetchData.ts @@ -0,0 +1,35 @@ +import fetch, { Request } from 'node-fetch'; +import { TypeGuard } from './typeGuard'; + +/** + * 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 typeGuard The type guard to ensure the retrieved data is of the expected type. The retrieved data will be converted to JSON and passed as argument to this function. + * **It is up to the developer to ensure the type guard works correctly!** + * @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). + */ +export const fetchData = async (request: Request, typeGuard: TypeGuard): Promise => { + try { + const response = await fetch(request); + + if (!response.ok) { + return Promise.reject(new Error(`HTTP Error. Status: ${response.status}`)); + } + + let data; + try { + data = await response.json(); + } catch (e) { + return Promise.reject(new Error(`Could not parse JSON. Make sure the endpoint at ${request.url} returns valid JSON. Error: ${String(e)}`)); + } + + if (!typeGuard(data)) { + return Promise.reject(new Error('Type guard not satisfied.')); + } + + return Promise.resolve(data as ReturnType); + } catch (e) { + return Promise.reject(new Error(`An error occured while fetching data from ${request.url}: ${String(e)}`)); + } +}; diff --git a/src/lib/apiWrapper/typeGuard.ts b/src/lib/apiWrapper/typeGuard.ts new file mode 100644 index 00000000..6f769f4b --- /dev/null +++ b/src/lib/apiWrapper/typeGuard.ts @@ -0,0 +1 @@ +export type TypeGuard = (value: unknown) => value is T; diff --git a/src/lib/index.ts b/src/lib/index.ts index c5983c58..7e3d3713 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -21,3 +21,7 @@ export * from './schemas/birthdaySchema'; export * from './schedulerJobs/autoDisableSlowMode'; export * from './schedulerJobs/sendHeartbeat'; export * from './schedulerJobs/postBirthdays'; + +// API Wrapper +export * from './apiWrapper/fetchData'; +export * from './apiWrapper/typeGuard'; From c15b31515e6eda82bebe9aeea17ab14b833e12ba Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 25 Jun 2024 07:28:10 +0200 Subject: [PATCH 02/34] add vatsim-event endpoint types --- src/lib/apiWrapper/types/vatsimEvents.ts | 133 +++++++++++++++++++++++ src/lib/index.ts | 1 + 2 files changed, 134 insertions(+) create mode 100644 src/lib/apiWrapper/types/vatsimEvents.ts diff --git a/src/lib/apiWrapper/types/vatsimEvents.ts b/src/lib/apiWrapper/types/vatsimEvents.ts new file mode 100644 index 00000000..20c03b75 --- /dev/null +++ b/src/lib/apiWrapper/types/vatsimEvents.ts @@ -0,0 +1,133 @@ +/* eslint-disable camelcase */ + +import { TypeGuard } from '../typeGuard'; + +/** + * @see https://vatsim.dev/api/events-api/1.0.0/list-events + */ +export interface VatsimEvents { + data: Data[]; +} + +export interface Data { + id: number; + type: 'Event' | 'Controller Examination' | 'VASOPS Event'; + name: string; + link: string; + organisers: Organiser[]; + airports: Airport[]; + routes: Route[]; + start_time: string; + end_time: string; + short_description: string; + description: string; + banner: string; +} + +export interface Organiser { + region: string | null; + division: string | null; + subdivision: string | null; + organised_by_vatsim: boolean +} + +export interface Airport { + icao: string; +} + +export interface Route { + departure: string; + arrival: string; + route: string; +} + +export const isVatsimEvents: TypeGuard = (events): events is VatsimEvents => { + if (typeof events !== 'object' || events === null) { + return false; + } + + if (!('data' in events)) { + return false; + } + + if (!Array.isArray(events.data)) { + return false; + } + + if (!events.data.every(isData)) { + return false; + } + + return true; +}; + +const isData: TypeGuard = (data): data is Data => { + if (typeof data !== 'object' || data === null) { + return false; + } + + if (!('organisers' in data) || !('airports' in data) || !('routes' in data)) { + return false; + } + + if (!Array.isArray(data.organisers) || !Array.isArray(data.airports) || !Array.isArray(data.routes)) { + return false; + } + + if (!data.organisers.every(isOrganiser)) { + return false; + } + + if (!data.airports.every(isAirport)) { + return false; + } + + if (!data.routes.every(isRoute)) { + return false; + } + + return ( + ('id' in data && typeof data.id === 'number') + && ('type' in data && (data.type === 'Event' || data.type === 'Controller Examination' || data.type === 'VASOPS Event')) + && ('name' in data && typeof data.name === 'string') + && ('link' in data && typeof data.link === 'string') + && ('start_time' in data && typeof data.start_time === 'string') + && ('end_time' in data && typeof data.end_time === 'string') + && ('short_description' in data && typeof data.short_description === 'string') + && ('description' in data && typeof data.description === 'string') + && ('banner' in data && typeof data.banner === 'string') + ); +}; + +const isOrganiser: TypeGuard = (organiser): organiser is Organiser => { + if (typeof organiser !== 'object' || organiser === null) { + return false; + } + + return ( + ('region' in organiser && (typeof organiser.region === 'string' || organiser.region === null)) + && ('division' in organiser && (typeof organiser.division === 'string' || organiser.division === null)) + && ('subdivision' in organiser && (typeof organiser.subdivision === 'string' || organiser.subdivision === null)) + && ('organised_by_vatsim' in organiser && typeof organiser.organised_by_vatsim === 'boolean') + ); +}; + +const isAirport: TypeGuard = (airport): airport is Airport => { + if (typeof airport !== 'object' || airport === null) { + return false; + } + + return ('icao' in airport && typeof airport.icao === 'string'); +}; + +const isRoute: TypeGuard = (route): route is Route => { + if (typeof route !== 'object' || route === null) { + return false; + } + + return ( + ('departure' in route && typeof route.departure === 'string') + && ('arrival' in route && typeof route.arrival === 'string') + && ('route' in route && typeof route.route === 'string') + ); +}; diff --git a/src/lib/index.ts b/src/lib/index.ts index 7e3d3713..3d373127 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -25,3 +25,4 @@ export * from './schedulerJobs/postBirthdays'; // API Wrapper export * from './apiWrapper/fetchData'; export * from './apiWrapper/typeGuard'; +export * from './apiWrapper/types/vatsimEvents'; From 211579de311ec4edd3ff15a03351e44dec6ef999 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 25 Jun 2024 08:18:50 +0200 Subject: [PATCH 03/34] add util type guards --- src/lib/apiWrapper/typeGuard.ts | 60 ++++++++++++++++++++++++ src/lib/apiWrapper/types/vatsimEvents.ts | 48 +++++++++---------- 2 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/lib/apiWrapper/typeGuard.ts b/src/lib/apiWrapper/typeGuard.ts index 6f769f4b..62915cc2 100644 --- a/src/lib/apiWrapper/typeGuard.ts +++ b/src/lib/apiWrapper/typeGuard.ts @@ -1 +1,61 @@ +/** + * Generic type guard. + */ export type TypeGuard = (value: unknown) => value is T; + +/** + * Check if a value is `null`. + * @param value The value to check. + * @returns `true` if the value is `null`, `false` otherwise. + */ +export const isNull: TypeGuard = (value: unknown): value is null => value === null; + +/** + * Check if a value is a true JavaScript object. + * @param value The value to check. + * @returns `true` if the value is a true JavaScript object, `false` otherwise. + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof + */ +export const isTrueObject: TypeGuard = (value: unknown): value is object => (typeof value === 'object' && value !== null && !isArray(value)); + +/** + * Check if a value is an array. This is a simple wrapper for [Array.isArray()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray). + * @param value The value to check. + * @returns `true` if the value is an array, `false` otherwise. + */ +export const isArray: TypeGuard = (value: unknown): value is any[] => Array.isArray(value); + +/** + * Check if a value is undefined. + * @param value The value to check. + * @returns `true` if the value is an array, `false` otherwise. + */ +export const isUndefined: TypeGuard = (value: unknown): value is undefined => typeof value === 'undefined'; + +/** + * Check if a value is a boolean. + * @param value The value to check. + * @returns `true` if the value is an array, `false` otherwise. + */ +export const isBoolean: TypeGuard = (value: unknown): value is boolean => typeof value === 'boolean'; + +/** + * Check if a value is a number. + * @param value The value to check. + * @returns `true` if the value is an array, `false` otherwise. + */ +export const isNumber: TypeGuard = (value: unknown): value is number => typeof value === 'number'; + +/** + * Check if a value is a bigint. + * @param value The value to check. + * @returns `true` if the value is an array, `false` otherwise. + */ +export const isBigInt: TypeGuard = (value: unknown): value is bigint => typeof value === 'bigint'; + +/** + * Check if the value is a string. + * @param value The value to check. + * @returns `true` if the value is an array, `false` otherwise. + */ +export const isString: TypeGuard = (value: unknown): value is string => typeof value === 'string'; diff --git a/src/lib/apiWrapper/types/vatsimEvents.ts b/src/lib/apiWrapper/types/vatsimEvents.ts index 20c03b75..992136fa 100644 --- a/src/lib/apiWrapper/types/vatsimEvents.ts +++ b/src/lib/apiWrapper/types/vatsimEvents.ts @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ -import { TypeGuard } from '../typeGuard'; +import { TypeGuard, isArray, isBoolean, isNull, isNumber, isString, isTrueObject } from '../typeGuard'; /** * @see https://vatsim.dev/api/events-api/1.0.0/list-events @@ -42,7 +42,7 @@ export interface Route { } export const isVatsimEvents: TypeGuard = (events): events is VatsimEvents => { - if (typeof events !== 'object' || events === null) { + if (isNull(events) || !isTrueObject(events)) { return false; } @@ -50,7 +50,7 @@ export const isVatsimEvents: TypeGuard = (events): events is Vatsi return false; } - if (!Array.isArray(events.data)) { + if (!isArray(events.data)) { return false; } @@ -62,7 +62,7 @@ export const isVatsimEvents: TypeGuard = (events): events is Vatsi }; const isData: TypeGuard = (data): data is Data => { - if (typeof data !== 'object' || data === null) { + if (isNull(data) || !isTrueObject(data)) { return false; } @@ -70,7 +70,7 @@ const isData: TypeGuard = (data): data is Data => { return false; } - if (!Array.isArray(data.organisers) || !Array.isArray(data.airports) || !Array.isArray(data.routes)) { + if (!isArray(data.organisers) || !isArray(data.airports) || !isArray(data.routes)) { return false; } @@ -87,47 +87,47 @@ const isData: TypeGuard = (data): data is Data => { } return ( - ('id' in data && typeof data.id === 'number') + ('id' in data && isNumber(data.id)) && ('type' in data && (data.type === 'Event' || data.type === 'Controller Examination' || data.type === 'VASOPS Event')) - && ('name' in data && typeof data.name === 'string') - && ('link' in data && typeof data.link === 'string') - && ('start_time' in data && typeof data.start_time === 'string') - && ('end_time' in data && typeof data.end_time === 'string') - && ('short_description' in data && typeof data.short_description === 'string') - && ('description' in data && typeof data.description === 'string') - && ('banner' in data && typeof data.banner === 'string') + && ('name' in data && isString(data.name)) + && ('link' in data && isString(data.link)) + && ('start_time' in data && isString(data.start_time)) + && ('end_time' in data && isString(data.end_time)) + && ('short_description' in data && isString(data.short_description)) + && ('description' in data && isString(data.description)) + && ('banner' in data && isString(data.banner)) ); }; const isOrganiser: TypeGuard = (organiser): organiser is Organiser => { - if (typeof organiser !== 'object' || organiser === null) { + if (isNull(organiser) || !isTrueObject(organiser)) { return false; } return ( - ('region' in organiser && (typeof organiser.region === 'string' || organiser.region === null)) - && ('division' in organiser && (typeof organiser.division === 'string' || organiser.division === null)) - && ('subdivision' in organiser && (typeof organiser.subdivision === 'string' || organiser.subdivision === null)) - && ('organised_by_vatsim' in organiser && typeof organiser.organised_by_vatsim === 'boolean') + ('region' in organiser && (isString(organiser.region) || isNull(organiser.region))) + && ('division' in organiser && (isString(organiser.division) || isNull(organiser.division))) + && ('subdivision' in organiser && (isString(organiser.subdivision) || isNull(organiser.subdivision))) + && ('organised_by_vatsim' in organiser && isBoolean(organiser.organised_by_vatsim)) ); }; const isAirport: TypeGuard = (airport): airport is Airport => { - if (typeof airport !== 'object' || airport === null) { + if (isNull(airport) || !isTrueObject(airport)) { return false; } - return ('icao' in airport && typeof airport.icao === 'string'); + return ('icao' in airport && isString(airport.icao)); }; const isRoute: TypeGuard = (route): route is Route => { - if (typeof route !== 'object' || route === null) { + if (isNull(route) || !isTrueObject(route)) { return false; } return ( - ('departure' in route && typeof route.departure === 'string') - && ('arrival' in route && typeof route.arrival === 'string') - && ('route' in route && typeof route.route === 'string') + ('departure' in route && isString(route.departure)) + && ('arrival' in route && isString(route.arrival)) + && ('route' in route && isString(route.route)) ); }; From 684ca62e6e2ca0a66cc141a34c91ab47f663d849 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 25 Jun 2024 08:31:11 +0200 Subject: [PATCH 04/34] migrate vatsim events to the new api wrapper --- .../utils/vatsim/functions/vatsimEvents.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/commands/utils/vatsim/functions/vatsimEvents.ts b/src/commands/utils/vatsim/functions/vatsimEvents.ts index 468c8f83..fc5042cd 100644 --- a/src/commands/utils/vatsim/functions/vatsimEvents.ts +++ b/src/commands/utils/vatsim/functions/vatsimEvents.ts @@ -1,5 +1,6 @@ import { ChatInputCommandInteraction, Colors, EmbedField } from 'discord.js'; -import { Logger, makeEmbed } from '../../../../lib'; +import { Request } from 'node-fetch'; +import { Logger, VatsimEvents, fetchData, isVatsimEvents, makeEmbed } from '../../../../lib'; const BASE_VATSIM_URL = 'https://my.vatsim.net'; @@ -18,13 +19,12 @@ export async function handleVatsimEvents(interaction: ChatInputCommandInteractio 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 fetchData(new Request(`${BASE_VATSIM_URL}/api/v1/events/all`), isVatsimEvents); - const fields: EmbedField[] = eventsList.map((event: any) => { + const filteredEvents = response.data.filter((event) => event.type === 'Event'); + const finalList = filteredEvents.slice(0, 5); + + 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 +71,11 @@ export async function handleVatsimEvents(interaction: ChatInputCommandInteractio }); return interaction.editReply({ embeds: [eventsEmbed] }); - } catch (error: any) { - Logger.error(error); + } catch (e) { + Logger.error(String(e)); const errorEmbed = makeEmbed({ title: 'Events Error', - description: error.message, + description: String(e), color: Colors.Red, }); return interaction.editReply({ embeds: [errorEmbed] }); From 7ab2383f1cbff8d94c54d39291f5ddff83d51e0f Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 25 Jun 2024 08:52:36 +0200 Subject: [PATCH 05/34] add vatsim data endpoint types --- src/lib/apiWrapper/types/vatsimData.ts | 142 +++++++++++++++++++++++ src/lib/apiWrapper/types/vatsimEvents.ts | 1 - 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/lib/apiWrapper/types/vatsimData.ts diff --git a/src/lib/apiWrapper/types/vatsimData.ts b/src/lib/apiWrapper/types/vatsimData.ts new file mode 100644 index 00000000..177f13c5 --- /dev/null +++ b/src/lib/apiWrapper/types/vatsimData.ts @@ -0,0 +1,142 @@ +/* eslint-disable camelcase */ + +/** + * @see https://vatsim.dev/api/data-api/get-network-data + */ +export interface VatsimData { + general: General; + pilots: Pilot[]; + controllers: Controller[]; + atis: Atis[]; + servers: Server[]; + prefiles: Prefile[]; + facilities: Facility[]; + ratings: Rating[]; + pilot_ratings: PilotRating[]; + military_ratings: MilitaryRating[]; +} + +export interface General { + version: number; + /** + * @deprecated + */ + reload: number; + /** + * @deprecated + */ + update: string; + update_timestamp: string; + connected_clients: number; + unique_users: number; +} + +export interface Pilot { + cid: number; + name: string; + callsign: string; + server: string; + pilot_rating: number; + military_rating: number; + latitude: number; + longitude: number; + altitude: number; + groundspeed: number; + transponder: string; + heading: number; + qnh_i_hg: number; + qnh_mb: number; + flight_plan: FlightPlan; + logon_time: string; + last_updated: string; +} + +export interface Controller { + cid: number; + name: string; + callsign: string; + frequency: string; + facility: number; + rating: number; + server: string; + visual_range: number; + text_atis: string[]; + last_updated: string; + logon_time: string; +} + +export interface Atis { + cid: number; + name: string; + callsign: string; + frequency: string; + facility: number; + rating: number; + server: string; + visual_range: number; + atis_code: string; + text_atis: string[]; + last_updated: string; + logon_time: string; +} + +export interface Server { + ident: string; + hostname_or_ip: string; + location: string; + name: string; + /** + * @deprecated + */ + clients_connection_allowed: string; + is_sweatbox: boolean; +} + +export interface Prefile { + cid: number; + name: string; + callsign: string; + flight_plan: FlightPlan; + last_updated: string; +} + +export interface FlightPlan { + flight_rules: 'I' | 'V'; + aircraft: string; + aircraft_faa: string; + aircraft_short: string; + departure: string; + arrival: string; + alternate: string; + deptime: string; + enroute_time: string; + fuel_time: string; + remarks: string; + route: string; + revision_id: number; + assigned_transponder: string; +} + +export interface Facility { + id: number; + short: string; + long_name: string; +} + +export interface Rating { + id: number; + short_name: string; + long_name: string; +} + +export interface PilotRating { + id: number; + short_name: string; + long_name: string; +} + +export interface MilitaryRating { + id: number; + short_name: string; + long_name: string; +} diff --git a/src/lib/apiWrapper/types/vatsimEvents.ts b/src/lib/apiWrapper/types/vatsimEvents.ts index 992136fa..2471d0ba 100644 --- a/src/lib/apiWrapper/types/vatsimEvents.ts +++ b/src/lib/apiWrapper/types/vatsimEvents.ts @@ -1,5 +1,4 @@ /* eslint-disable camelcase */ - import { TypeGuard, isArray, isBoolean, isNull, isNumber, isString, isTrueObject } from '../typeGuard'; /** From bd67559db01138e8e92c0a299f7331784bfc301d Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:16:28 +0200 Subject: [PATCH 06/34] add zod library --- package-lock.json | 11 ++++++++++- package.json | 3 ++- src/lib/apis/fetchData.ts | 37 +++++++++++++++++++++++++++++++++++++ src/lib/index.ts | 4 +--- 4 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 src/lib/apis/fetchData.ts diff --git a/package-lock.json b/package-lock.json index 1da2a388..59c56e41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,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", @@ -7692,6 +7693,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 20845ba2..c050fd15 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,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/lib/apis/fetchData.ts b/src/lib/apis/fetchData.ts new file mode 100644 index 00000000..9732b65a --- /dev/null +++ b/src/lib/apis/fetchData.ts @@ -0,0 +1,37 @@ +import fetch, { Request } from 'node-fetch'; +import { ZodSchema } from 'zod'; + +/** + * 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 typeGuard The type guard to ensure the retrieved data is of the expected type. The retrieved data will be converted to JSON and passed as argument to this function. + * **It is up to the developer to ensure the type guard works correctly!** + * @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). + */ +export const fetchData = async (request: Request, schema: ZodSchema): Promise => { + try { + const response = await fetch(request); + + if (!response.ok) { + return Promise.reject(new Error(`HTTP Error. Status: ${response.status}`)); + } + + let data; + try { + data = await response.json(); + } catch (e) { + return Promise.reject(new Error(`Could not parse JSON. Make sure the endpoint at ${request.url} returns valid JSON. Error: ${String(e)}`)); + } + + const result = schema.safeParse(data); + + if (!result.success) { + return Promise.reject(result.error); + } + + return Promise.resolve(result.data); + } catch (e) { + return Promise.reject(new Error(`An error occured while fetching data from ${request.url}: ${String(e)}`)); + } +}; diff --git a/src/lib/index.ts b/src/lib/index.ts index 3d373127..794fc7a9 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -23,6 +23,4 @@ export * from './schedulerJobs/sendHeartbeat'; export * from './schedulerJobs/postBirthdays'; // API Wrapper -export * from './apiWrapper/fetchData'; -export * from './apiWrapper/typeGuard'; -export * from './apiWrapper/types/vatsimEvents'; +export * from './apis/fetchData'; From bd0b412c962c1f08241ec135fcb90e8aa6701764 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 01:19:44 +0200 Subject: [PATCH 07/34] partial vatsim port to zod --- .../vatsim/functions/vatsimControllers.ts | 61 +++---- .../utils/vatsim/functions/vatsimEvents.ts | 4 +- src/commands/utils/vatsim/vatsim.ts | 51 +++--- src/lib/apiWrapper/fetchData.ts | 35 ---- src/lib/apiWrapper/typeGuard.ts | 61 ------- src/lib/apiWrapper/types/vatsimData.ts | 142 ---------------- src/lib/apiWrapper/types/vatsimEvents.ts | 132 --------------- src/lib/apis/zodSchemas/vatsimData.ts | 156 ++++++++++++++++++ src/lib/apis/zodSchemas/vatsimEvents.ts | 38 +++++ src/lib/index.ts | 2 + 10 files changed, 245 insertions(+), 437 deletions(-) delete mode 100644 src/lib/apiWrapper/fetchData.ts delete mode 100644 src/lib/apiWrapper/typeGuard.ts delete mode 100644 src/lib/apiWrapper/types/vatsimData.ts delete mode 100644 src/lib/apiWrapper/types/vatsimEvents.ts create mode 100644 src/lib/apis/zodSchemas/vatsimData.ts create mode 100644 src/lib/apis/zodSchemas/vatsimEvents.ts diff --git a/src/commands/utils/vatsim/functions/vatsimControllers.ts b/src/commands/utils/vatsim/functions/vatsimControllers.ts index 08e5cd2b..4396ae5a 100644 --- a/src/commands/utils/vatsim/functions/vatsimControllers.ts +++ b/src/commands/utils/vatsim/functions/vatsimControllers.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, EmbedField } from 'discord.js'; -import { makeEmbed } from '../../../../lib'; +import { Atis, Rating, VatsimData, makeEmbed } from '../../../../lib'; /* eslint-disable camelcase */ @@ -9,7 +9,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 +26,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) { fields.push({ - name: atisTitle, - value: atis, + name: `ATIS - Code: ${atis.atis_code}`, + value: atis.text_atis.join('\n'), inline: false, }); } @@ -57,30 +55,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: EmbedField[] = 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).flat(); - return interaction.reply({ embeds: [listEmbed('Controllers & ATIS', fields, totalCount, shownCount, callsignSearch)] }); + return interaction.reply({ embeds: [listEmbed('Controllers & ATIS', fields, controllers.length, fields.length, callsignSearch)] }); } diff --git a/src/commands/utils/vatsim/functions/vatsimEvents.ts b/src/commands/utils/vatsim/functions/vatsimEvents.ts index fc5042cd..3ac53a1b 100644 --- a/src/commands/utils/vatsim/functions/vatsimEvents.ts +++ b/src/commands/utils/vatsim/functions/vatsimEvents.ts @@ -1,6 +1,6 @@ import { ChatInputCommandInteraction, Colors, EmbedField } from 'discord.js'; import { Request } from 'node-fetch'; -import { Logger, VatsimEvents, fetchData, isVatsimEvents, makeEmbed } from '../../../../lib'; +import { Logger, VatsimEvents, VatsimEventsSchema, fetchData, makeEmbed } from '../../../../lib'; const BASE_VATSIM_URL = 'https://my.vatsim.net'; @@ -19,7 +19,7 @@ export async function handleVatsimEvents(interaction: ChatInputCommandInteractio await interaction.deferReply(); try { - const response = await fetchData(new Request(`${BASE_VATSIM_URL}/api/v1/events/all`), isVatsimEvents); + const response = await fetchData(new Request(`${BASE_VATSIM_URL}/api/v1/events/all`), VatsimEventsSchema); const filteredEvents = response.data.filter((event) => event.type === 'Event'); const finalList = filteredEvents.slice(0, 5); diff --git a/src/commands/utils/vatsim/vatsim.ts b/src/commands/utils/vatsim/vatsim.ts index c1511937..3ff964c1 100644 --- a/src/commands/utils/vatsim/vatsim.ts +++ b/src/commands/utils/vatsim/vatsim.ts @@ -1,10 +1,12 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; -import { makeEmbed, slashCommand, slashCommandStructure } from '../../../lib'; -import { handleVatsimStats } from './functions/vatsimStats'; +import { Request } from 'node-fetch'; +import { ZodError } from 'zod'; +import { Logger, VatsimData, VatsimDataSchema, fetchData, 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', @@ -86,24 +88,21 @@ const fetchErrorEmbed = (error: any) => makeEmbed({ export default slashCommand(data, async ({ interaction }) => { // Fetch VATSIM data - - let vatsimData; + 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 fetchData(new Request('https://data.vatsim.net/v3/vatsim-data.json'), VatsimDataSchema); + } catch (e) { + if (e instanceof ZodError) { + e.issues.forEach((e) => Logger.error(`[zod Issue VATSIM Data] Code: ${e.code}, Path: ${e.path.join('.')}, Message: ${e.message}`)); + return interaction.reply({ embeds: [fetchErrorEmbed('The VATSIM API returned unknown data.')] }); + } + + return interaction.reply({ embeds: [fetchErrorEmbed(e)], ephemeral: true }); } // Grap the callsign from the interaction - let callsign = interaction.options.getString('callsign'); - let callsignSearch; + let callsignSearch: string | undefined; if (callsign) { callsign = callsign.toUpperCase(); @@ -112,7 +111,6 @@ 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 }); } @@ -128,19 +126,18 @@ export default slashCommand(data, async ({ interaction }) => { await handleVatsimStats(interaction, vatsimData, callsignSearch); break; case 'controllers': - await handleVatsimControllers(interaction, vatsimData, callsignSearch); - break; + if (!callsignSearch) { + return interaction.reply({ content: 'You need to provide a valid callsign or part of a callsign to search for', ephemeral: true }); + } + return handleVatsimControllers(interaction, vatsimData, callsignSearch); case 'pilots': - await handleVatsimPilots(interaction, vatsimData, callsignSearch); - break; + return handleVatsimPilots(interaction, vatsimData, callsignSearch); case 'observers': - await handleVatsimObservers(interaction, vatsimData, callsignSearch); - break; + return handleVatsimObservers(interaction, vatsimData, callsignSearch); case 'events': - await handleVatsimEvents(interaction); - break; + return handleVatsimEvents(interaction); default: - await interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); + return interaction.reply({ content: 'Unknown subcommand', ephemeral: true }); } }); diff --git a/src/lib/apiWrapper/fetchData.ts b/src/lib/apiWrapper/fetchData.ts deleted file mode 100644 index 1c840e05..00000000 --- a/src/lib/apiWrapper/fetchData.ts +++ /dev/null @@ -1,35 +0,0 @@ -import fetch, { Request } from 'node-fetch'; -import { TypeGuard } from './typeGuard'; - -/** - * 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 typeGuard The type guard to ensure the retrieved data is of the expected type. The retrieved data will be converted to JSON and passed as argument to this function. - * **It is up to the developer to ensure the type guard works correctly!** - * @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). - */ -export const fetchData = async (request: Request, typeGuard: TypeGuard): Promise => { - try { - const response = await fetch(request); - - if (!response.ok) { - return Promise.reject(new Error(`HTTP Error. Status: ${response.status}`)); - } - - let data; - try { - data = await response.json(); - } catch (e) { - return Promise.reject(new Error(`Could not parse JSON. Make sure the endpoint at ${request.url} returns valid JSON. Error: ${String(e)}`)); - } - - if (!typeGuard(data)) { - return Promise.reject(new Error('Type guard not satisfied.')); - } - - return Promise.resolve(data as ReturnType); - } catch (e) { - return Promise.reject(new Error(`An error occured while fetching data from ${request.url}: ${String(e)}`)); - } -}; diff --git a/src/lib/apiWrapper/typeGuard.ts b/src/lib/apiWrapper/typeGuard.ts deleted file mode 100644 index 62915cc2..00000000 --- a/src/lib/apiWrapper/typeGuard.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * Generic type guard. - */ -export type TypeGuard = (value: unknown) => value is T; - -/** - * Check if a value is `null`. - * @param value The value to check. - * @returns `true` if the value is `null`, `false` otherwise. - */ -export const isNull: TypeGuard = (value: unknown): value is null => value === null; - -/** - * Check if a value is a true JavaScript object. - * @param value The value to check. - * @returns `true` if the value is a true JavaScript object, `false` otherwise. - * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof - */ -export const isTrueObject: TypeGuard = (value: unknown): value is object => (typeof value === 'object' && value !== null && !isArray(value)); - -/** - * Check if a value is an array. This is a simple wrapper for [Array.isArray()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray). - * @param value The value to check. - * @returns `true` if the value is an array, `false` otherwise. - */ -export const isArray: TypeGuard = (value: unknown): value is any[] => Array.isArray(value); - -/** - * Check if a value is undefined. - * @param value The value to check. - * @returns `true` if the value is an array, `false` otherwise. - */ -export const isUndefined: TypeGuard = (value: unknown): value is undefined => typeof value === 'undefined'; - -/** - * Check if a value is a boolean. - * @param value The value to check. - * @returns `true` if the value is an array, `false` otherwise. - */ -export const isBoolean: TypeGuard = (value: unknown): value is boolean => typeof value === 'boolean'; - -/** - * Check if a value is a number. - * @param value The value to check. - * @returns `true` if the value is an array, `false` otherwise. - */ -export const isNumber: TypeGuard = (value: unknown): value is number => typeof value === 'number'; - -/** - * Check if a value is a bigint. - * @param value The value to check. - * @returns `true` if the value is an array, `false` otherwise. - */ -export const isBigInt: TypeGuard = (value: unknown): value is bigint => typeof value === 'bigint'; - -/** - * Check if the value is a string. - * @param value The value to check. - * @returns `true` if the value is an array, `false` otherwise. - */ -export const isString: TypeGuard = (value: unknown): value is string => typeof value === 'string'; diff --git a/src/lib/apiWrapper/types/vatsimData.ts b/src/lib/apiWrapper/types/vatsimData.ts deleted file mode 100644 index 177f13c5..00000000 --- a/src/lib/apiWrapper/types/vatsimData.ts +++ /dev/null @@ -1,142 +0,0 @@ -/* eslint-disable camelcase */ - -/** - * @see https://vatsim.dev/api/data-api/get-network-data - */ -export interface VatsimData { - general: General; - pilots: Pilot[]; - controllers: Controller[]; - atis: Atis[]; - servers: Server[]; - prefiles: Prefile[]; - facilities: Facility[]; - ratings: Rating[]; - pilot_ratings: PilotRating[]; - military_ratings: MilitaryRating[]; -} - -export interface General { - version: number; - /** - * @deprecated - */ - reload: number; - /** - * @deprecated - */ - update: string; - update_timestamp: string; - connected_clients: number; - unique_users: number; -} - -export interface Pilot { - cid: number; - name: string; - callsign: string; - server: string; - pilot_rating: number; - military_rating: number; - latitude: number; - longitude: number; - altitude: number; - groundspeed: number; - transponder: string; - heading: number; - qnh_i_hg: number; - qnh_mb: number; - flight_plan: FlightPlan; - logon_time: string; - last_updated: string; -} - -export interface Controller { - cid: number; - name: string; - callsign: string; - frequency: string; - facility: number; - rating: number; - server: string; - visual_range: number; - text_atis: string[]; - last_updated: string; - logon_time: string; -} - -export interface Atis { - cid: number; - name: string; - callsign: string; - frequency: string; - facility: number; - rating: number; - server: string; - visual_range: number; - atis_code: string; - text_atis: string[]; - last_updated: string; - logon_time: string; -} - -export interface Server { - ident: string; - hostname_or_ip: string; - location: string; - name: string; - /** - * @deprecated - */ - clients_connection_allowed: string; - is_sweatbox: boolean; -} - -export interface Prefile { - cid: number; - name: string; - callsign: string; - flight_plan: FlightPlan; - last_updated: string; -} - -export interface FlightPlan { - flight_rules: 'I' | 'V'; - aircraft: string; - aircraft_faa: string; - aircraft_short: string; - departure: string; - arrival: string; - alternate: string; - deptime: string; - enroute_time: string; - fuel_time: string; - remarks: string; - route: string; - revision_id: number; - assigned_transponder: string; -} - -export interface Facility { - id: number; - short: string; - long_name: string; -} - -export interface Rating { - id: number; - short_name: string; - long_name: string; -} - -export interface PilotRating { - id: number; - short_name: string; - long_name: string; -} - -export interface MilitaryRating { - id: number; - short_name: string; - long_name: string; -} diff --git a/src/lib/apiWrapper/types/vatsimEvents.ts b/src/lib/apiWrapper/types/vatsimEvents.ts deleted file mode 100644 index 2471d0ba..00000000 --- a/src/lib/apiWrapper/types/vatsimEvents.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* eslint-disable camelcase */ -import { TypeGuard, isArray, isBoolean, isNull, isNumber, isString, isTrueObject } from '../typeGuard'; - -/** - * @see https://vatsim.dev/api/events-api/1.0.0/list-events - */ -export interface VatsimEvents { - data: Data[]; -} - -export interface Data { - id: number; - type: 'Event' | 'Controller Examination' | 'VASOPS Event'; - name: string; - link: string; - organisers: Organiser[]; - airports: Airport[]; - routes: Route[]; - start_time: string; - end_time: string; - short_description: string; - description: string; - banner: string; -} - -export interface Organiser { - region: string | null; - division: string | null; - subdivision: string | null; - organised_by_vatsim: boolean -} - -export interface Airport { - icao: string; -} - -export interface Route { - departure: string; - arrival: string; - route: string; -} - -export const isVatsimEvents: TypeGuard = (events): events is VatsimEvents => { - if (isNull(events) || !isTrueObject(events)) { - return false; - } - - if (!('data' in events)) { - return false; - } - - if (!isArray(events.data)) { - return false; - } - - if (!events.data.every(isData)) { - return false; - } - - return true; -}; - -const isData: TypeGuard = (data): data is Data => { - if (isNull(data) || !isTrueObject(data)) { - return false; - } - - if (!('organisers' in data) || !('airports' in data) || !('routes' in data)) { - return false; - } - - if (!isArray(data.organisers) || !isArray(data.airports) || !isArray(data.routes)) { - return false; - } - - if (!data.organisers.every(isOrganiser)) { - return false; - } - - if (!data.airports.every(isAirport)) { - return false; - } - - if (!data.routes.every(isRoute)) { - return false; - } - - return ( - ('id' in data && isNumber(data.id)) - && ('type' in data && (data.type === 'Event' || data.type === 'Controller Examination' || data.type === 'VASOPS Event')) - && ('name' in data && isString(data.name)) - && ('link' in data && isString(data.link)) - && ('start_time' in data && isString(data.start_time)) - && ('end_time' in data && isString(data.end_time)) - && ('short_description' in data && isString(data.short_description)) - && ('description' in data && isString(data.description)) - && ('banner' in data && isString(data.banner)) - ); -}; - -const isOrganiser: TypeGuard = (organiser): organiser is Organiser => { - if (isNull(organiser) || !isTrueObject(organiser)) { - return false; - } - - return ( - ('region' in organiser && (isString(organiser.region) || isNull(organiser.region))) - && ('division' in organiser && (isString(organiser.division) || isNull(organiser.division))) - && ('subdivision' in organiser && (isString(organiser.subdivision) || isNull(organiser.subdivision))) - && ('organised_by_vatsim' in organiser && isBoolean(organiser.organised_by_vatsim)) - ); -}; - -const isAirport: TypeGuard = (airport): airport is Airport => { - if (isNull(airport) || !isTrueObject(airport)) { - return false; - } - - return ('icao' in airport && isString(airport.icao)); -}; - -const isRoute: TypeGuard = (route): route is Route => { - if (isNull(route) || !isTrueObject(route)) { - return false; - } - - return ( - ('departure' in route && isString(route.departure)) - && ('arrival' in route && isString(route.arrival)) - && ('route' in route && isString(route.route)) - ); -}; diff --git a/src/lib/apis/zodSchemas/vatsimData.ts b/src/lib/apis/zodSchemas/vatsimData.ts new file mode 100644 index 00000000..c0319f6c --- /dev/null +++ b/src/lib/apis/zodSchemas/vatsimData.ts @@ -0,0 +1,156 @@ +import { z } from 'zod'; + +const MilitaryRatingSchema = z.object({ + id: z.number(), + short_name: z.string(), + long_name: z.string(), +}); + +const PilotRatingSchema = z.object({ + id: z.number(), + short_name: z.string(), + long_name: z.string(), +}); + +const RatingSchema = z.object({ + id: z.number(), + short: z.string(), + long: z.string(), +}); + +const FacilitySchema = z.object({ + id: z.number(), + short: z.string(), + long: z.string(), +}); + +const FlightPlanSchema = 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(), +}); + +const PrefileSchema = z.object({ + cid: z.number(), + name: z.string(), + callsign: z.string(), + flight_plan: FlightPlanSchema, + last_updated: z.string(), +}); + +const ServerSchema = 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(), +}); + +const AtisSchema = 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.array(z.string()), + last_updated: z.string(), + logon_time: z.string(), +}); + +const ControllerSchema = 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(), +}); + +const PilotSchema = 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(FlightPlanSchema), + logon_time: z.string(), + last_updated: z.string(), +}); + +const GeneralSchema = 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: GeneralSchema, + pilots: z.array(PilotSchema), + controllers: z.array(ControllerSchema), + atis: z.array(AtisSchema), + servers: z.array(ServerSchema), + prefiles: z.array(PrefileSchema), + facilities: z.array(FacilitySchema), + ratings: z.array(RatingSchema), + pilot_ratings: z.array(PilotRatingSchema), + military_ratings: z.array(MilitaryRatingSchema), +}); + +export type VatsimData = z.infer; +export type General = z.infer; +export type Pilot = z.infer; +export type Controller = z.infer; +export type Atis = z.infer; +export type Server = z.infer; +export type Prefiles = z.infer; +export type Facility = z.infer; +export type Rating = z.infer; +export type PilotRating = z.infer; +export type MilitaryRating = z.infer; diff --git a/src/lib/apis/zodSchemas/vatsimEvents.ts b/src/lib/apis/zodSchemas/vatsimEvents.ts new file mode 100644 index 00000000..64ebc4e6 --- /dev/null +++ b/src/lib/apis/zodSchemas/vatsimEvents.ts @@ -0,0 +1,38 @@ +import { z } from 'zod'; + +export const OrganiserSchema = z.object({ + region: z.nullable(z.string()), + division: z.nullable(z.string()), + subdivision: z.nullable(z.string()), + organised_by_vatsim: z.boolean(), +}); + +export const AirportSchema = z.object({ icao: z.string() }); + +export const RouteSchema = z.object({ + departure: z.string(), + arrival: z.string(), + route: z.string(), +}); + +export const DataSchema = z.object({ + id: z.number(), + type: z.enum(['Event', 'Controller Examination', 'VASOPS Event']), + name: z.string(), + link: z.string(), + organisers: z.array(OrganiserSchema), + airports: z.array(AirportSchema), + routes: z.array(RouteSchema), + 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(DataSchema) }); + +export type VatsimEvents = z.infer; diff --git a/src/lib/index.ts b/src/lib/index.ts index 794fc7a9..710177e6 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -24,3 +24,5 @@ export * from './schedulerJobs/postBirthdays'; // API Wrapper export * from './apis/fetchData'; +export * from './apis/zodSchemas/vatsimEvents'; +export * from './apis/zodSchemas/vatsimData'; From 0f7ec6184cc60d78c8527fe849915289ba931b15 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 02:12:12 +0200 Subject: [PATCH 08/34] migrate vatsim observer to zod --- .../vatsim/functions/vatsimControllers.ts | 10 ++-- .../utils/vatsim/functions/vatsimObservers.ts | 48 ++++++------------- src/commands/utils/vatsim/vatsim.ts | 15 ++++-- src/lib/apis/zodSchemas/vatsimData.ts | 2 +- 4 files changed, 31 insertions(+), 44 deletions(-) diff --git a/src/commands/utils/vatsim/functions/vatsimControllers.ts b/src/commands/utils/vatsim/functions/vatsimControllers.ts index 4396ae5a..23c620d1 100644 --- a/src/commands/utils/vatsim/functions/vatsimControllers.ts +++ b/src/commands/utils/vatsim/functions/vatsimControllers.ts @@ -36,9 +36,9 @@ const controllersListEmbedFields = (callsign: string, frequency: string, logon: }); } - if (atis) { + if (atis && atis.text_atis) { fields.push({ - name: `ATIS - Code: ${atis.atis_code}`, + name: `ATIS - ${atis.atis_code ? atis.atis_code : 'N/A'}`, value: atis.text_atis.join('\n'), inline: false, }); @@ -59,13 +59,13 @@ export async function handleVatsimControllers(interaction: ChatInputCommandInter const controllers = vatsimData.controllers.filter((controller) => controller.facility > 0 && controller.callsign.includes(callsignSearch)); controllers.sort((a, b) => b.facility - a.facility); - const fields: EmbedField[] = controllers.map((controller) => { + 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); return controllersListEmbedFields(callsign, frequency, handleLocaleDateTimeString(new Date(logon_time)), rating, atis); - }).splice(0, 5).flat(); + }).splice(0, 5); - return interaction.reply({ embeds: [listEmbed('Controllers & ATIS', fields, controllers.length, fields.length, callsignSearch)] }); + return interaction.editReply({ embeds: [listEmbed('Controllers & ATIS', fields.flat(), controllers.length, fields.length, callsignSearch)] }); } diff --git a/src/commands/utils/vatsim/functions/vatsimObservers.ts b/src/commands/utils/vatsim/functions/vatsimObservers.ts index 48fd6ed1..6b0f330b 100644 --- a/src/commands/utils/vatsim/functions/vatsimObservers.ts +++ b/src/commands/utils/vatsim/functions/vatsimObservers.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, EmbedField } from 'discord.js'; -import { makeEmbed } from '../../../../lib'; +import { Rating, VatsimData, makeEmbed } from '../../../../lib'; /* eslint-disable camelcase */ @@ -9,7 +9,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 +21,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 +42,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/vatsim.ts b/src/commands/utils/vatsim/vatsim.ts index 3ff964c1..71f0a007 100644 --- a/src/commands/utils/vatsim/vatsim.ts +++ b/src/commands/utils/vatsim/vatsim.ts @@ -87,6 +87,8 @@ const fetchErrorEmbed = (error: any) => makeEmbed({ }); export default slashCommand(data, async ({ interaction }) => { + await interaction.deferReply(); + // Fetch VATSIM data let vatsimData: VatsimData; try { @@ -94,10 +96,10 @@ export default slashCommand(data, async ({ interaction }) => { } catch (e) { if (e instanceof ZodError) { e.issues.forEach((e) => Logger.error(`[zod Issue VATSIM Data] Code: ${e.code}, Path: ${e.path.join('.')}, Message: ${e.message}`)); - return interaction.reply({ embeds: [fetchErrorEmbed('The VATSIM API returned unknown data.')] }); + return interaction.editReply({ embeds: [fetchErrorEmbed('The VATSIM API returned unknown data.')] }); } - return interaction.reply({ embeds: [fetchErrorEmbed(e)], ephemeral: true }); + return interaction.editReply({ embeds: [fetchErrorEmbed(e)] }); } // Grap the callsign from the interaction @@ -111,7 +113,7 @@ export default slashCommand(data, async ({ interaction }) => { const regexMatches = callsign.match(regexCheck); if (!regexMatches || !regexMatches.groups || !regexMatches.groups.callsignSearch) { - 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; @@ -127,17 +129,20 @@ export default slashCommand(data, async ({ interaction }) => { break; case 'controllers': if (!callsignSearch) { - 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.' }); } return handleVatsimControllers(interaction, vatsimData, callsignSearch); case 'pilots': 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.reply({ content: 'Unknown subcommand', ephemeral: true }); + return interaction.editReply({ content: 'Unknown subcommand' }); } }); diff --git a/src/lib/apis/zodSchemas/vatsimData.ts b/src/lib/apis/zodSchemas/vatsimData.ts index c0319f6c..dda25fb9 100644 --- a/src/lib/apis/zodSchemas/vatsimData.ts +++ b/src/lib/apis/zodSchemas/vatsimData.ts @@ -72,7 +72,7 @@ const AtisSchema = z.object({ server: z.string(), visual_range: z.number(), atis_code: z.nullable(z.string()), - text_atis: z.array(z.string()), + text_atis: z.nullable(z.array(z.string())), last_updated: z.string(), logon_time: z.string(), }); From 992c700518d16dde36c6de4d2904017c7b529b8e Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 02:13:20 +0200 Subject: [PATCH 09/34] add missing flight plan type export --- src/lib/apis/zodSchemas/vatsimData.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/apis/zodSchemas/vatsimData.ts b/src/lib/apis/zodSchemas/vatsimData.ts index dda25fb9..2bf7b9d2 100644 --- a/src/lib/apis/zodSchemas/vatsimData.ts +++ b/src/lib/apis/zodSchemas/vatsimData.ts @@ -150,6 +150,7 @@ export type Controller = z.infer; export type Atis = z.infer; export type Server = z.infer; export type Prefiles = z.infer; +export type FlightPlan = z.infer; export type Facility = z.infer; export type Rating = z.infer; export type PilotRating = z.infer; From 81b904557d166d9392d84359da4a67c49abf332b Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 02:36:24 +0200 Subject: [PATCH 10/34] migrate vatsim pilot to zod --- .../utils/vatsim/functions/vatsimPilots.ts | 42 +++++++++---------- src/commands/utils/vatsim/vatsim.ts | 3 ++ 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/commands/utils/vatsim/functions/vatsimPilots.ts b/src/commands/utils/vatsim/functions/vatsimPilots.ts index e73c6300..e78eae5c 100644 --- a/src/commands/utils/vatsim/functions/vatsimPilots.ts +++ b/src/commands/utils/vatsim/functions/vatsimPilots.ts @@ -1,5 +1,5 @@ import { ChatInputCommandInteraction, EmbedField } from 'discord.js'; -import { makeEmbed } from '../../../../lib'; +import { FlightPlan, PilotRating, VatsimData, makeEmbed } from '../../../../lib'; /* eslint-disable camelcase */ @@ -8,21 +8,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, rating?: PilotRating, flightPlan?: FlightPlan) => { 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 +44,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, rating, flight_plan ?? undefined); + }).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/vatsim.ts b/src/commands/utils/vatsim/vatsim.ts index 71f0a007..82e8a905 100644 --- a/src/commands/utils/vatsim/vatsim.ts +++ b/src/commands/utils/vatsim/vatsim.ts @@ -133,6 +133,9 @@ export default slashCommand(data, async ({ interaction }) => { } 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) { From 7b37d6ee0664fc17b2786bf84d863e3cc6d5f4d1 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 02:50:08 +0200 Subject: [PATCH 11/34] migrate vatsim stats to zod --- .../utils/vatsim/functions/vatsimStats.ts | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) 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)] }); } From 7062fc00673189074f8a6855b85f74b61153354b Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 02:56:15 +0200 Subject: [PATCH 12/34] fix lint in vatsim --- src/commands/utils/vatsim/vatsim.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commands/utils/vatsim/vatsim.ts b/src/commands/utils/vatsim/vatsim.ts index 82e8a905..d830f38e 100644 --- a/src/commands/utils/vatsim/vatsim.ts +++ b/src/commands/utils/vatsim/vatsim.ts @@ -148,4 +148,7 @@ export default slashCommand(data, async ({ interaction }) => { default: return interaction.editReply({ content: 'Unknown subcommand' }); } + + // Done to satisfy ESLint + return interaction.editReply({ content: 'Unknown subcommend' }); }); From 5e2a3b791ffd440f30f416b9ecd2b9162ea32419 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 03:15:53 +0200 Subject: [PATCH 13/34] migrate vatsimEvents --- .../utils/vatsim/functions/vatsimEvents.ts | 2 -- src/commands/utils/vatsim/vatsim.ts | 31 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/commands/utils/vatsim/functions/vatsimEvents.ts b/src/commands/utils/vatsim/functions/vatsimEvents.ts index 3ac53a1b..45161a74 100644 --- a/src/commands/utils/vatsim/functions/vatsimEvents.ts +++ b/src/commands/utils/vatsim/functions/vatsimEvents.ts @@ -16,8 +16,6 @@ const handleLocaleDateString = (date: Date) => date.toLocaleDateString('en-US', }); export async function handleVatsimEvents(interaction: ChatInputCommandInteraction<'cached'>) { - await interaction.deferReply(); - try { const response = await fetchData(new Request(`${BASE_VATSIM_URL}/api/v1/events/all`), VatsimEventsSchema); diff --git a/src/commands/utils/vatsim/vatsim.ts b/src/commands/utils/vatsim/vatsim.ts index d830f38e..6505be1f 100644 --- a/src/commands/utils/vatsim/vatsim.ts +++ b/src/commands/utils/vatsim/vatsim.ts @@ -95,11 +95,10 @@ export default slashCommand(data, async ({ interaction }) => { vatsimData = await fetchData(new Request('https://data.vatsim.net/v3/vatsim-data.json'), VatsimDataSchema); } catch (e) { if (e instanceof ZodError) { - e.issues.forEach((e) => Logger.error(`[zod Issue VATSIM Data] Code: ${e.code}, Path: ${e.path.join('.')}, Message: ${e.message}`)); + e.issues.forEach((issue) => Logger.error(`[zod Issue VATSIM Data] Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); return interaction.editReply({ embeds: [fetchErrorEmbed('The VATSIM API returned unknown data.')] }); } - - return interaction.editReply({ embeds: [fetchErrorEmbed(e)] }); + return interaction.editReply({ embeds: [fetchErrorEmbed('The VATSIM API returned unknown data.')] }); } // Grap the callsign from the interaction @@ -120,35 +119,35 @@ export default slashCommand(data, async ({ interaction }) => { } // Handle the subcommands - const subcommandName = interaction.options.getSubcommand(); switch (subcommandName) { - case 'stats': - await handleVatsimStats(interaction, vatsimData, callsignSearch); - break; - case 'controllers': + 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': + } + 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': + } + 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': + } + case 'events': { return handleVatsimEvents(interaction); - - default: + } + default: { return interaction.editReply({ content: 'Unknown subcommand' }); } - - // Done to satisfy ESLint - return interaction.editReply({ content: 'Unknown subcommend' }); + } }); From 33e3219634a85c4da026be79541329cf8e908064 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 03:16:07 +0200 Subject: [PATCH 14/34] improve var naming --- src/lib/apis/fetchData.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/apis/fetchData.ts b/src/lib/apis/fetchData.ts index 9732b65a..fd403ad7 100644 --- a/src/lib/apis/fetchData.ts +++ b/src/lib/apis/fetchData.ts @@ -9,7 +9,7 @@ import { ZodSchema } from 'zod'; * **It is up to the developer to ensure the type guard works correctly!** * @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). */ -export const fetchData = async (request: Request, schema: ZodSchema): Promise => { +export const fetchData = async (request: Request, zodSchema: ZodSchema): Promise => { try { const response = await fetch(request); @@ -24,7 +24,7 @@ export const fetchData = async (request: Request, schema: ZodSchema< return Promise.reject(new Error(`Could not parse JSON. Make sure the endpoint at ${request.url} returns valid JSON. Error: ${String(e)}`)); } - const result = schema.safeParse(data); + const result = zodSchema.safeParse(data); if (!result.success) { return Promise.reject(result.error); From 7a0c89bed1cf1e180c0696246d5421b32b483e39 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 03:21:55 +0200 Subject: [PATCH 15/34] refactor fetchData --- src/lib/apis/fetchData.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib/apis/fetchData.ts b/src/lib/apis/fetchData.ts index fd403ad7..837176ba 100644 --- a/src/lib/apis/fetchData.ts +++ b/src/lib/apis/fetchData.ts @@ -5,33 +5,32 @@ import { ZodSchema } from 'zod'; * 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 typeGuard The type guard to ensure the retrieved data is of the expected type. The retrieved data will be converted to JSON and passed as argument to this function. - * **It is up to the developer to ensure the type guard works correctly!** + * @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). */ -export const fetchData = async (request: Request, zodSchema: ZodSchema): Promise => { +export const fetchData = async (request: Request, zodSchema: ZodSchema): Promise => { try { const response = await fetch(request); if (!response.ok) { - return Promise.reject(new Error(`HTTP Error. Status: ${response.status}`)); + throw new Error(`HTTP Error. Status: ${response.status}`); } let data; try { data = await response.json(); } catch (e) { - return Promise.reject(new Error(`Could not parse JSON. Make sure the endpoint at ${request.url} returns valid JSON. Error: ${String(e)}`)); + throw new Error(`Could not parse JSON. Make sure the endpoint at ${request.url} returns valid JSON. Error: ${String(e)}`); } const result = zodSchema.safeParse(data); if (!result.success) { - return Promise.reject(result.error); + throw result.error; } - return Promise.resolve(result.data); + return result.data; } catch (e) { - return Promise.reject(new Error(`An error occured while fetching data from ${request.url}: ${String(e)}`)); + throw new Error(`An error occured while fetching data from ${request.url}: ${String(e)}`); } }; From 31c2ea39b8f179e7e33b2fca47d05db40cd0e7d0 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Tue, 2 Jul 2024 03:30:09 +0200 Subject: [PATCH 16/34] remove any ewww --- src/lib/apis/fetchData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/apis/fetchData.ts b/src/lib/apis/fetchData.ts index 837176ba..ac224707 100644 --- a/src/lib/apis/fetchData.ts +++ b/src/lib/apis/fetchData.ts @@ -16,7 +16,7 @@ export const fetchData = async (request: Request, zodSchem throw new Error(`HTTP Error. Status: ${response.status}`); } - let data; + let data: unknown; try { data = await response.json(); } catch (e) { From 540557ad8ef68dadacf515eae5353d92a6d6f0f3 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:02:11 +0200 Subject: [PATCH 17/34] migrate taf cmd to zod --- src/commands/utils/taf.ts | 64 ++++++++----------- src/lib/apis/fetchData.ts | 3 + src/lib/apis/zodSchemas/tafSchemas.ts | 12 ++++ .../{vatsimData.ts => vatsimDataSchemas.ts} | 0 ...vatsimEvents.ts => vatsimEventsSchemas.ts} | 0 src/lib/index.ts | 5 +- 6 files changed, 46 insertions(+), 38 deletions(-) create mode 100644 src/lib/apis/zodSchemas/tafSchemas.ts rename src/lib/apis/zodSchemas/{vatsimData.ts => vatsimDataSchemas.ts} (100%) rename src/lib/apis/zodSchemas/{vatsimEvents.ts => vatsimEventsSchemas.ts} (100%) diff --git a/src/commands/utils/taf.ts b/src/commands/utils/taf.ts index dba64551..6e6a8fe9 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, fetchData, 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,47 +49,32 @@ 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 fetchData(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, + 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}`, - ]), + try { + 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 raw report?', - value: 'Please refer to our guide [here.](https://docs.flybywiresim.com/pilots-corner/airliner-flying-guide/weather/#taf-example-decoded)', + 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, }, ], diff --git a/src/lib/apis/fetchData.ts b/src/lib/apis/fetchData.ts index ac224707..e9d6af7e 100644 --- a/src/lib/apis/fetchData.ts +++ b/src/lib/apis/fetchData.ts @@ -1,5 +1,6 @@ import fetch, { Request } from 'node-fetch'; import { ZodSchema } from 'zod'; +import { Logger } from '../logger'; /** * Fetch data from any API endpoint that returns JSON formatted data. @@ -26,6 +27,8 @@ export const fetchData = async (request: Request, zodSchem const result = zodSchema.safeParse(data); if (!result.success) { + Logger.error('[zod] Data validation failed:'); + result.error.issues.forEach((issue) => Logger.error(`Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); throw result.error; } diff --git a/src/lib/apis/zodSchemas/tafSchemas.ts b/src/lib/apis/zodSchemas/tafSchemas.ts new file mode 100644 index 00000000..00189909 --- /dev/null +++ b/src/lib/apis/zodSchemas/tafSchemas.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const ForecastSchema = z.object({ raw: z.string() }); + +export const TafSchema = z.object({ + raw: z.string(), + station: z.string(), + forecast: z.array(ForecastSchema), +}); + +export type Forecast = z.infer; +export type TAF = z.infer; diff --git a/src/lib/apis/zodSchemas/vatsimData.ts b/src/lib/apis/zodSchemas/vatsimDataSchemas.ts similarity index 100% rename from src/lib/apis/zodSchemas/vatsimData.ts rename to src/lib/apis/zodSchemas/vatsimDataSchemas.ts diff --git a/src/lib/apis/zodSchemas/vatsimEvents.ts b/src/lib/apis/zodSchemas/vatsimEventsSchemas.ts similarity index 100% rename from src/lib/apis/zodSchemas/vatsimEvents.ts rename to src/lib/apis/zodSchemas/vatsimEventsSchemas.ts diff --git a/src/lib/index.ts b/src/lib/index.ts index 710177e6..da883890 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -24,5 +24,6 @@ export * from './schedulerJobs/postBirthdays'; // API Wrapper export * from './apis/fetchData'; -export * from './apis/zodSchemas/vatsimEvents'; -export * from './apis/zodSchemas/vatsimData'; +export * from './apis/zodSchemas/vatsimEventsSchemas'; +export * from './apis/zodSchemas/vatsimDataSchemas'; +export * from './apis/zodSchemas/tafSchemas'; From 0bffea0a5cffd67303ee29b463be75a91611b8d4 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:39:43 +0200 Subject: [PATCH 18/34] migrate metar cmd to zod --- src/commands/utils/metar.ts | 93 ++++++++++++------------- src/commands/utils/taf.ts | 48 +++++-------- src/lib/apis/zodSchemas/metarSchemas.ts | 46 ++++++++++++ src/lib/apis/zodSchemas/tafSchemas.ts | 1 - 4 files changed, 110 insertions(+), 78 deletions(-) create mode 100644 src/lib/apis/zodSchemas/metarSchemas.ts diff --git a/src/commands/utils/metar.ts b/src/commands/utils/metar.ts index dd688e48..16c73992 100644 --- a/src/commands/utils/metar.ts +++ b/src/commands/utils/metar.ts @@ -1,6 +1,8 @@ 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, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; +import { Metar, MetarSchema } from '../../lib/apis/zodSchemas/metarSchemas'; const data = slashCommandStructure({ name: 'metar', @@ -16,6 +18,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 +40,44 @@ 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 fetchData(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.')] }); + } + 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/taf.ts b/src/commands/utils/taf.ts index 6e6a8fe9..635a5a53 100644 --- a/src/commands/utils/taf.ts +++ b/src/commands/utils/taf.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { Logger, TAF, TafSchema, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; +import { TAF, TafSchema, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; const data = slashCommandStructure({ name: 'taf', @@ -62,33 +62,23 @@ export default slashCommand(data, async ({ interaction }) => { return interaction.editReply({ embeds: [errorEmbed(`An error occurred while fetching the latest TAF for ${icao.toUpperCase()}.`)] }); } - try { - 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.' }, - }); + 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/lib/apis/zodSchemas/metarSchemas.ts b/src/lib/apis/zodSchemas/metarSchemas.ts new file mode 100644 index 00000000..76a6b072 --- /dev/null +++ b/src/lib/apis/zodSchemas/metarSchemas.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +export const TimeSchema = z.object({ dt: z.string().datetime() }); + +export const WindDirectionSchema = z.object({ repr: z.string() }); + +export const WindSpeedSchema = z.object({ repr: z.string() }); + +export const VisibilitySchema = z.object({ repr: z.string() }); + +export const TemperatureSchema = z.object({ repr: z.string() }); + +export const DewpointSchema = z.object({ repr: z.string() }); + +export const AltimeterSchema = z.object({ value: z.number() }); + +export const UnitsSchema = 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 below. + */ +export const MetarSchema = z.object({ + station: z.string(), + raw: z.string(), + time: TimeSchema, + wind_direction: WindDirectionSchema, + wind_speed: WindSpeedSchema, + visibility: VisibilitySchema, + temperature: TemperatureSchema, + dewpoint: DewpointSchema, + altimeter: AltimeterSchema, + flight_rules: z.string(), + units: UnitsSchema, +}); + +/** + * This type only contains currently used fields. If you wish to use other fields returned by the API add them below. + */ +export type Metar = z.infer; diff --git a/src/lib/apis/zodSchemas/tafSchemas.ts b/src/lib/apis/zodSchemas/tafSchemas.ts index 00189909..c9b5992d 100644 --- a/src/lib/apis/zodSchemas/tafSchemas.ts +++ b/src/lib/apis/zodSchemas/tafSchemas.ts @@ -8,5 +8,4 @@ export const TafSchema = z.object({ forecast: z.array(ForecastSchema), }); -export type Forecast = z.infer; export type TAF = z.infer; From 5c1299b5528922479eca59e991f6e4e08f9c607f Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:42:25 +0200 Subject: [PATCH 19/34] add disclaimer comments to taf and metar schemas --- src/lib/apis/zodSchemas/metarSchemas.ts | 4 ++-- src/lib/apis/zodSchemas/tafSchemas.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib/apis/zodSchemas/metarSchemas.ts b/src/lib/apis/zodSchemas/metarSchemas.ts index 76a6b072..bccb80fe 100644 --- a/src/lib/apis/zodSchemas/metarSchemas.ts +++ b/src/lib/apis/zodSchemas/metarSchemas.ts @@ -24,7 +24,7 @@ export const UnitsSchema = z.object({ }); /** - * This schema only contains currently used fields. If you wish to use other fields returned by the API add them below. + * 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(), @@ -41,6 +41,6 @@ export const MetarSchema = z.object({ }); /** - * This type only contains currently used fields. If you wish to use other fields returned by the API add them below. + * 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/tafSchemas.ts b/src/lib/apis/zodSchemas/tafSchemas.ts index c9b5992d..0fcb6001 100644 --- a/src/lib/apis/zodSchemas/tafSchemas.ts +++ b/src/lib/apis/zodSchemas/tafSchemas.ts @@ -2,10 +2,15 @@ import { z } from 'zod'; export const ForecastSchema = 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(ForecastSchema), }); - +/** + * 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; From f24dd5c27b956951995ba8ee818f3c2cb1fe93d3 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Wed, 3 Jul 2024 21:38:46 +0200 Subject: [PATCH 20/34] migrate simbrief cmd to zod --- src/commands/utils/simbriefData.ts | 37 +++++++----- .../vatsim/functions/vatsimControllers.ts | 6 +- .../utils/vatsim/functions/vatsimObservers.ts | 5 +- .../utils/vatsim/functions/vatsimPilots.ts | 6 +- src/lib/apis/zodSchemas/metarSchemas.ts | 32 +++++------ src/lib/apis/zodSchemas/simbriefSchemas.ts | 37 ++++++++++++ src/lib/apis/zodSchemas/tafSchemas.ts | 4 +- src/lib/apis/zodSchemas/vatsimDataSchemas.ts | 57 ++++++++----------- .../apis/zodSchemas/vatsimEventsSchemas.ts | 16 +++--- src/lib/index.ts | 1 + 10 files changed, 125 insertions(+), 76 deletions(-) create mode 100644 src/lib/apis/zodSchemas/simbriefSchemas.ts diff --git a/src/commands/utils/simbriefData.ts b/src/commands/utils/simbriefData.ts index b7d818a1..0a31e074 100644 --- a/src/commands/utils/simbriefData.ts +++ b/src/commands/utils/simbriefData.ts @@ -1,6 +1,8 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import moment from 'moment'; -import { slashCommand, makeEmbed, makeLines, slashCommandStructure } from '../../lib'; +import { Request } from 'node-fetch'; +import { ZodError } from 'zod'; +import { slashCommand, makeEmbed, makeLines, slashCommandStructure, SimbriefFlightPlan, fetchData, SimbriefFlightPlanSchema } from '../../lib'; const data = slashCommandStructure({ name: 'simbrief-data', @@ -40,9 +42,9 @@ const simbriefdatarequestEmbed = makeEmbed({ ]), }); -const errorEmbed = (errorMessage: any) => makeEmbed({ +const errorEmbed = (errorMessage: string) => makeEmbed({ title: 'SimBrief Error', - description: makeLines(['SimBrief data could not be read.', errorMessage]), + description: errorMessage, color: Colors.Red, }); @@ -53,10 +55,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 +73,32 @@ 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 fetchData(new Request(`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.')] }); + } + 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/vatsim/functions/vatsimControllers.ts b/src/commands/utils/vatsim/functions/vatsimControllers.ts index 23c620d1..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 { Atis, Rating, VatsimData, 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 */ diff --git a/src/commands/utils/vatsim/functions/vatsimObservers.ts b/src/commands/utils/vatsim/functions/vatsimObservers.ts index 6b0f330b..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 { Rating, VatsimData, makeEmbed } from '../../../../lib'; +import { z } from 'zod'; +import { VatsimRatingSchema, VatsimData, makeEmbed } from '../../../../lib'; + +type Rating = z.infer; /* eslint-disable camelcase */ diff --git a/src/commands/utils/vatsim/functions/vatsimPilots.ts b/src/commands/utils/vatsim/functions/vatsimPilots.ts index e78eae5c..31096301 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 { FlightPlan, PilotRating, VatsimData, 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 */ diff --git a/src/lib/apis/zodSchemas/metarSchemas.ts b/src/lib/apis/zodSchemas/metarSchemas.ts index bccb80fe..fcf39046 100644 --- a/src/lib/apis/zodSchemas/metarSchemas.ts +++ b/src/lib/apis/zodSchemas/metarSchemas.ts @@ -1,20 +1,20 @@ import { z } from 'zod'; -export const TimeSchema = z.object({ dt: z.string().datetime() }); +export const MetarTimeSchema = z.object({ dt: z.string().datetime() }); -export const WindDirectionSchema = z.object({ repr: z.string() }); +export const MetarWindDirectionSchema = z.object({ repr: z.string() }); -export const WindSpeedSchema = z.object({ repr: z.string() }); +export const MetarWindSpeedSchema = z.object({ repr: z.string() }); -export const VisibilitySchema = z.object({ repr: z.string() }); +export const MetarVisibilitySchema = z.object({ repr: z.string() }); -export const TemperatureSchema = z.object({ repr: z.string() }); +export const MetarTemperatureSchema = z.object({ repr: z.string() }); -export const DewpointSchema = z.object({ repr: z.string() }); +export const MetarDewpointSchema = z.object({ repr: z.string() }); -export const AltimeterSchema = z.object({ value: z.number() }); +export const MetarAltimeterSchema = z.object({ value: z.number() }); -export const UnitsSchema = z.object({ +export const MetarUnitsSchema = z.object({ accumulation: z.string(), altimeter: z.string(), altitude: z.string(), @@ -29,15 +29,15 @@ export const UnitsSchema = z.object({ export const MetarSchema = z.object({ station: z.string(), raw: z.string(), - time: TimeSchema, - wind_direction: WindDirectionSchema, - wind_speed: WindSpeedSchema, - visibility: VisibilitySchema, - temperature: TemperatureSchema, - dewpoint: DewpointSchema, - altimeter: AltimeterSchema, + time: MetarTimeSchema, + wind_direction: MetarWindDirectionSchema, + wind_speed: MetarWindSpeedSchema, + visibility: MetarVisibilitySchema, + temperature: MetarTemperatureSchema, + dewpoint: MetarDewpointSchema, + altimeter: MetarAltimeterSchema, flight_rules: z.string(), - units: UnitsSchema, + units: MetarUnitsSchema, }); /** diff --git a/src/lib/apis/zodSchemas/simbriefSchemas.ts b/src/lib/apis/zodSchemas/simbriefSchemas.ts new file mode 100644 index 00000000..07b71b4a --- /dev/null +++ b/src/lib/apis/zodSchemas/simbriefSchemas.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; + +const FetchSchema = z.object({ status: z.string() }); + +const ParamsSchema = z.object({ + user_id: z.string(), + time_generated: z.string(), + airac: z.string(), +}); + +const AircraftSchema = z.object({ + name: z.string(), + internal_id: z.string(), +}); + +const OriginSchema = z.object({ icao_code: z.string() }); + +const DestinationSchema = z.object({ icao_code: z.string() }); + +const GeneralSchema = 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: FetchSchema, + params: ParamsSchema, + aircraft: AircraftSchema, + origin: OriginSchema, + destination: DestinationSchema, + general: GeneralSchema, +}); + +/** + * 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/tafSchemas.ts b/src/lib/apis/zodSchemas/tafSchemas.ts index 0fcb6001..69356b74 100644 --- a/src/lib/apis/zodSchemas/tafSchemas.ts +++ b/src/lib/apis/zodSchemas/tafSchemas.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -export const ForecastSchema = z.object({ raw: z.string() }); +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. @@ -8,7 +8,7 @@ export const ForecastSchema = z.object({ raw: z.string() }); export const TafSchema = z.object({ raw: z.string(), station: z.string(), - forecast: z.array(ForecastSchema), + 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. diff --git a/src/lib/apis/zodSchemas/vatsimDataSchemas.ts b/src/lib/apis/zodSchemas/vatsimDataSchemas.ts index 2bf7b9d2..aaed9092 100644 --- a/src/lib/apis/zodSchemas/vatsimDataSchemas.ts +++ b/src/lib/apis/zodSchemas/vatsimDataSchemas.ts @@ -1,30 +1,30 @@ import { z } from 'zod'; -const MilitaryRatingSchema = z.object({ +export const VatsimMilitaryRatingSchema = z.object({ id: z.number(), short_name: z.string(), long_name: z.string(), }); -const PilotRatingSchema = z.object({ +export const VatsimPilotRatingSchema = z.object({ id: z.number(), short_name: z.string(), long_name: z.string(), }); -const RatingSchema = z.object({ +export const VatsimRatingSchema = z.object({ id: z.number(), short: z.string(), long: z.string(), }); -const FacilitySchema = z.object({ +export const VatsimFacilitySchema = z.object({ id: z.number(), short: z.string(), long: z.string(), }); -const FlightPlanSchema = z.object({ +export const VatsimFlightPlanSchema = z.object({ flight_rules: z.enum(['I', 'V']), aircraft: z.string(), aircraft_faa: z.string(), @@ -41,15 +41,15 @@ const FlightPlanSchema = z.object({ assigned_transponder: z.string(), }); -const PrefileSchema = z.object({ +export const VatsimPrefileSchema = z.object({ cid: z.number(), name: z.string(), callsign: z.string(), - flight_plan: FlightPlanSchema, + flight_plan: VatsimFlightPlanSchema, last_updated: z.string(), }); -const ServerSchema = z.object({ +export const VatsimServerSchema = z.object({ ident: z.string(), hostname_or_ip: z.string(), location: z.string(), @@ -62,7 +62,7 @@ const ServerSchema = z.object({ is_sweatbox: z.boolean(), }); -const AtisSchema = z.object({ +export const VatsimAtisSchema = z.object({ cid: z.number(), name: z.string(), callsign: z.string(), @@ -77,7 +77,7 @@ const AtisSchema = z.object({ logon_time: z.string(), }); -const ControllerSchema = z.object({ +export const VatsimControllerSchema = z.object({ cid: z.number(), name: z.string(), callsign: z.string(), @@ -91,7 +91,7 @@ const ControllerSchema = z.object({ logon_time: z.string(), }); -const PilotSchema = z.object({ +export const VatsimPilotSchema = z.object({ cid: z.number(), name: z.string(), callsign: z.string(), @@ -106,12 +106,12 @@ const PilotSchema = z.object({ heading: z.number(), qnh_i_hg: z.number(), qnh_mb: z.number(), - flight_plan: z.nullable(FlightPlanSchema), + flight_plan: z.nullable(VatsimFlightPlanSchema), logon_time: z.string(), last_updated: z.string(), }); -const GeneralSchema = z.object({ +export const VatsimGeneralSchema = z.object({ version: z.number(), /** * @deprecated @@ -131,27 +131,16 @@ const GeneralSchema = z.object({ * @see https://vatsim.dev/api/data-api/get-network-data */ export const VatsimDataSchema = z.object({ - general: GeneralSchema, - pilots: z.array(PilotSchema), - controllers: z.array(ControllerSchema), - atis: z.array(AtisSchema), - servers: z.array(ServerSchema), - prefiles: z.array(PrefileSchema), - facilities: z.array(FacilitySchema), - ratings: z.array(RatingSchema), - pilot_ratings: z.array(PilotRatingSchema), - military_ratings: z.array(MilitaryRatingSchema), + 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; -export type General = z.infer; -export type Pilot = z.infer; -export type Controller = z.infer; -export type Atis = z.infer; -export type Server = z.infer; -export type Prefiles = z.infer; -export type FlightPlan = z.infer; -export type Facility = z.infer; -export type Rating = z.infer; -export type PilotRating = z.infer; -export type MilitaryRating = z.infer; diff --git a/src/lib/apis/zodSchemas/vatsimEventsSchemas.ts b/src/lib/apis/zodSchemas/vatsimEventsSchemas.ts index 64ebc4e6..d4122445 100644 --- a/src/lib/apis/zodSchemas/vatsimEventsSchemas.ts +++ b/src/lib/apis/zodSchemas/vatsimEventsSchemas.ts @@ -1,28 +1,28 @@ import { z } from 'zod'; -export const OrganiserSchema = z.object({ +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 AirportSchema = z.object({ icao: z.string() }); +export const VatsimAirportSchema = z.object({ icao: z.string() }); -export const RouteSchema = z.object({ +export const VatsimRouteSchema = z.object({ departure: z.string(), arrival: z.string(), route: z.string(), }); -export const DataSchema = z.object({ +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(OrganiserSchema), - airports: z.array(AirportSchema), - routes: z.array(RouteSchema), + organisers: z.array(VatsimOrganiserSchema), + airports: z.array(VatsimAirportSchema), + routes: z.array(VatsimRouteSchema), start_time: z.string(), end_time: z.string(), short_description: z.string(), @@ -33,6 +33,6 @@ export const DataSchema = z.object({ /** * @see https://vatsim.dev/api/events-api/1.0.0/list-events */ -export const VatsimEventsSchema = z.object({ data: z.array(DataSchema) }); +export const VatsimEventsSchema = z.object({ data: z.array(VatsimEventsDataSchema) }); export type VatsimEvents = z.infer; diff --git a/src/lib/index.ts b/src/lib/index.ts index da883890..841eaca6 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -27,3 +27,4 @@ export * from './apis/fetchData'; export * from './apis/zodSchemas/vatsimEventsSchemas'; export * from './apis/zodSchemas/vatsimDataSchemas'; export * from './apis/zodSchemas/tafSchemas'; +export * from './apis/zodSchemas/simbriefSchemas'; From 50d08dc39a6797eb860e2999b78b7d37070405c5 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 01:30:18 +0200 Subject: [PATCH 21/34] migrate wolfram alpha cmd to zod --- src/commands/utils/wolframAlpha.ts | 115 +++++++++--------- .../apis/zodSchemas/wolframAlphaSchemas.ts | 50 ++++++++ src/lib/index.ts | 1 + 3 files changed, 109 insertions(+), 57 deletions(-) create mode 100644 src/lib/apis/zodSchemas/wolframAlphaSchemas.ts diff --git a/src/commands/utils/wolframAlpha.ts b/src/commands/utils/wolframAlpha.ts index 71ed2744..9e95e063 100644 --- a/src/commands/utils/wolframAlpha.ts +++ b/src/commands/utils/wolframAlpha.ts @@ -1,5 +1,10 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; -import { slashCommand, slashCommandStructure, makeEmbed, makeLines, Logger } from '../../lib'; +import { Request } from 'node-fetch'; +import { z, ZodError } from 'zod'; +import { fetchData, 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 +24,12 @@ const noQueryEmbed = makeEmbed({ color: Colors.Red, }); +const errorEmbed = (errorMessage: string) => makeEmbed({ + title: 'Wolfram Alpha Error', + description: errorMessage, + color: Colors.Red, +}); + const WOLFRAMALPHA_API_URL = 'https://api.wolframalpha.com/v2/query?'; const WOLFRAMALPHA_QUERY_URL = 'https://www.wolframalpha.com/input/?'; @@ -33,12 +44,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 +60,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 fetchData(new Request(`${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/zodSchemas/wolframAlphaSchemas.ts b/src/lib/apis/zodSchemas/wolframAlphaSchemas.ts new file mode 100644 index 00000000..07d937bb --- /dev/null +++ b/src/lib/apis/zodSchemas/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 841eaca6..7c6f1c8b 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -28,3 +28,4 @@ export * from './apis/zodSchemas/vatsimEventsSchemas'; export * from './apis/zodSchemas/vatsimDataSchemas'; export * from './apis/zodSchemas/tafSchemas'; export * from './apis/zodSchemas/simbriefSchemas'; +export * from './apis/zodSchemas/wolframAlphaSchemas'; From 0044d3c00b11519796cad77e67582bd53c3856ce Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 01:30:37 +0200 Subject: [PATCH 22/34] fix zod error being thrown incorrectly while fetching data --- src/lib/apis/fetchData.ts | 44 ++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/lib/apis/fetchData.ts b/src/lib/apis/fetchData.ts index e9d6af7e..8aaa00fd 100644 --- a/src/lib/apis/fetchData.ts +++ b/src/lib/apis/fetchData.ts @@ -1,4 +1,4 @@ -import fetch, { Request } from 'node-fetch'; +import fetch, { Request, Response } from 'node-fetch'; import { ZodSchema } from 'zod'; import { Logger } from '../logger'; @@ -10,30 +10,32 @@ import { Logger } from '../logger'; * @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). */ export const fetchData = async (request: Request, zodSchema: ZodSchema): Promise => { + let response: Response; try { - const response = await fetch(request); - - if (!response.ok) { - throw new Error(`HTTP Error. Status: ${response.status}`); - } + response = await fetch(request); + } catch (e) { + throw new Error(`An error occured while fetching data from ${request.url}: ${String(e)}`); + } - let data: unknown; - try { - data = await response.json(); - } catch (e) { - throw new Error(`Could not parse JSON. Make sure the endpoint at ${request.url} returns valid JSON. Error: ${String(e)}`); - } + if (!response.ok) { + throw new Error(`HTTP Error. Status: ${response.status}`); + } - const result = zodSchema.safeParse(data); + let data: unknown; + try { + data = await response.json(); + } catch (e) { + throw new Error(`Could not parse JSON. Make sure the endpoint at ${request.url} returns valid JSON. Error: ${String(e)}`); + } - if (!result.success) { - Logger.error('[zod] Data validation failed:'); - result.error.issues.forEach((issue) => Logger.error(`Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); - throw result.error; - } + const result = zodSchema.safeParse(data); - return result.data; - } catch (e) { - throw new Error(`An error occured while fetching data from ${request.url}: ${String(e)}`); + if (!result.success) { + Logger.error('[zod] Data validation failed:'); + result.error.issues.forEach((issue) => Logger.error(`Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); + console.log(data); + throw result.error; } + + return result.data; }; From d1a8704fe26078f8926b997a8ed23c3d8f840997 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 01:33:46 +0200 Subject: [PATCH 23/34] add folder structure to zod schema directory --- src/commands/utils/metar.ts | 2 +- src/lib/apis/zodSchemas/{ => avwx}/metarSchemas.ts | 0 src/lib/apis/zodSchemas/{ => avwx}/tafSchemas.ts | 0 .../apis/zodSchemas/{ => simbrief}/simbriefSchemas.ts | 0 .../apis/zodSchemas/{ => vatsim}/vatsimDataSchemas.ts | 0 .../zodSchemas/{ => vatsim}/vatsimEventsSchemas.ts | 0 .../{ => wolframAlpha}/wolframAlphaSchemas.ts | 0 src/lib/index.ts | 10 +++++----- 8 files changed, 6 insertions(+), 6 deletions(-) rename src/lib/apis/zodSchemas/{ => avwx}/metarSchemas.ts (100%) rename src/lib/apis/zodSchemas/{ => avwx}/tafSchemas.ts (100%) rename src/lib/apis/zodSchemas/{ => simbrief}/simbriefSchemas.ts (100%) rename src/lib/apis/zodSchemas/{ => vatsim}/vatsimDataSchemas.ts (100%) rename src/lib/apis/zodSchemas/{ => vatsim}/vatsimEventsSchemas.ts (100%) rename src/lib/apis/zodSchemas/{ => wolframAlpha}/wolframAlphaSchemas.ts (100%) diff --git a/src/commands/utils/metar.ts b/src/commands/utils/metar.ts index 16c73992..019e8da4 100644 --- a/src/commands/utils/metar.ts +++ b/src/commands/utils/metar.ts @@ -2,7 +2,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'di import { Request } from 'node-fetch'; import { ZodError } from 'zod'; import { constantsConfig, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; -import { Metar, MetarSchema } from '../../lib/apis/zodSchemas/metarSchemas'; +import { Metar, MetarSchema } from '../../lib/apis/zodSchemas/avwx/metarSchemas'; const data = slashCommandStructure({ name: 'metar', diff --git a/src/lib/apis/zodSchemas/metarSchemas.ts b/src/lib/apis/zodSchemas/avwx/metarSchemas.ts similarity index 100% rename from src/lib/apis/zodSchemas/metarSchemas.ts rename to src/lib/apis/zodSchemas/avwx/metarSchemas.ts diff --git a/src/lib/apis/zodSchemas/tafSchemas.ts b/src/lib/apis/zodSchemas/avwx/tafSchemas.ts similarity index 100% rename from src/lib/apis/zodSchemas/tafSchemas.ts rename to src/lib/apis/zodSchemas/avwx/tafSchemas.ts diff --git a/src/lib/apis/zodSchemas/simbriefSchemas.ts b/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts similarity index 100% rename from src/lib/apis/zodSchemas/simbriefSchemas.ts rename to src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts diff --git a/src/lib/apis/zodSchemas/vatsimDataSchemas.ts b/src/lib/apis/zodSchemas/vatsim/vatsimDataSchemas.ts similarity index 100% rename from src/lib/apis/zodSchemas/vatsimDataSchemas.ts rename to src/lib/apis/zodSchemas/vatsim/vatsimDataSchemas.ts diff --git a/src/lib/apis/zodSchemas/vatsimEventsSchemas.ts b/src/lib/apis/zodSchemas/vatsim/vatsimEventsSchemas.ts similarity index 100% rename from src/lib/apis/zodSchemas/vatsimEventsSchemas.ts rename to src/lib/apis/zodSchemas/vatsim/vatsimEventsSchemas.ts diff --git a/src/lib/apis/zodSchemas/wolframAlphaSchemas.ts b/src/lib/apis/zodSchemas/wolframAlpha/wolframAlphaSchemas.ts similarity index 100% rename from src/lib/apis/zodSchemas/wolframAlphaSchemas.ts rename to src/lib/apis/zodSchemas/wolframAlpha/wolframAlphaSchemas.ts diff --git a/src/lib/index.ts b/src/lib/index.ts index 7c6f1c8b..40f92c7c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -24,8 +24,8 @@ export * from './schedulerJobs/postBirthdays'; // API Wrapper export * from './apis/fetchData'; -export * from './apis/zodSchemas/vatsimEventsSchemas'; -export * from './apis/zodSchemas/vatsimDataSchemas'; -export * from './apis/zodSchemas/tafSchemas'; -export * from './apis/zodSchemas/simbriefSchemas'; -export * from './apis/zodSchemas/wolframAlphaSchemas'; +export * from './apis/zodSchemas/vatsim/vatsimEventsSchemas'; +export * from './apis/zodSchemas/vatsim/vatsimDataSchemas'; +export * from './apis/zodSchemas/avwx/tafSchemas'; +export * from './apis/zodSchemas/simbrief/simbriefSchemas'; +export * from './apis/zodSchemas/wolframAlpha/wolframAlphaSchemas'; From c841feaa702be1b2b9bbebde038d8eecd8e0016c Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 02:05:08 +0200 Subject: [PATCH 24/34] migrate station cmd to zod --- src/commands/utils/station.ts | 89 +++++++++---------- src/commands/utils/taf.ts | 3 +- .../apis/zodSchemas/avwx/stationSchemas.ts | 25 ++++++ src/lib/index.ts | 2 + 4 files changed, 73 insertions(+), 46 deletions(-) create mode 100644 src/lib/apis/zodSchemas/avwx/stationSchemas.ts diff --git a/src/commands/utils/station.ts b/src/commands/utils/station.ts index 8cde14a2..5965391d 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, fetchData, 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 fetchData(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 635a5a53..417c7d2e 100644 --- a/src/commands/utils/taf.ts +++ b/src/commands/utils/taf.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { TAF, TafSchema, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; +import { Logger, TAF, TafSchema, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; const data = slashCommandStructure({ name: 'taf', @@ -59,6 +59,7 @@ export default slashCommand(data, async ({ interaction }) => { if (e instanceof ZodError) { return interaction.editReply({ embeds: [errorEmbed('The API returned unknown data.')] }); } + 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()}.`)] }); } 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/index.ts b/src/lib/index.ts index 40f92c7c..d536b3bb 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -26,6 +26,8 @@ export * from './schedulerJobs/postBirthdays'; export * from './apis/fetchData'; 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'; From 698074e20df304cb4f00095c712ed3ada36d2fb7 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 02:15:25 +0200 Subject: [PATCH 25/34] migrate live-flight cmd to zod --- src/commands/utils/liveFlights.ts | 20 ++++++++++++++++---- src/commands/utils/metar.ts | 3 +-- src/lib/apis/zodSchemas/flybywire/telex.ts | 5 +++++ src/lib/index.ts | 1 + 4 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 src/lib/apis/zodSchemas/flybywire/telex.ts diff --git a/src/commands/utils/liveFlights.ts b/src/commands/utils/liveFlights.ts index cc0e0b96..574b08e4 100644 --- a/src/commands/utils/liveFlights.ts +++ b/src/commands/utils/liveFlights.ts @@ -1,5 +1,7 @@ import { ApplicationCommandType, Colors } from 'discord.js'; -import { slashCommand, slashCommandStructure, makeEmbed, Logger } from '../../lib'; +import { Request } from 'node-fetch'; +import { ZodError } from 'zod'; +import { slashCommand, slashCommandStructure, makeEmbed, Logger, fetchData, TelexCountSchema } from '../../lib'; const data = slashCommandStructure({ name: 'live-flights', @@ -11,8 +13,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 fetchData(new Request(`${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 +24,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 +41,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 019e8da4..de014134 100644 --- a/src/commands/utils/metar.ts +++ b/src/commands/utils/metar.ts @@ -1,8 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { constantsConfig, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; -import { Metar, MetarSchema } from '../../lib/apis/zodSchemas/avwx/metarSchemas'; +import { constantsConfig, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure, Metar, MetarSchema } from '../../lib'; const data = slashCommandStructure({ name: 'metar', 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/index.ts b/src/lib/index.ts index d536b3bb..66854ddf 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -31,3 +31,4 @@ 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'; From eae9c22ececcefbcaf9f4d00112f9c4cd004ad70 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 02:20:40 +0200 Subject: [PATCH 26/34] rename fetchData to fetchForeignAPI --- src/commands/utils/liveFlights.ts | 4 ++-- src/commands/utils/metar.ts | 4 ++-- src/commands/utils/simbriefData.ts | 4 ++-- src/commands/utils/station.ts | 4 ++-- src/commands/utils/taf.ts | 4 ++-- src/commands/utils/vatsim/functions/vatsimEvents.ts | 4 ++-- src/commands/utils/vatsim/vatsim.ts | 4 ++-- src/commands/utils/wolframAlpha.ts | 4 ++-- src/lib/apis/{fetchData.ts => fetchForeignAPI.ts} | 4 ++-- src/lib/index.ts | 2 +- 10 files changed, 19 insertions(+), 19 deletions(-) rename src/lib/apis/{fetchData.ts => fetchForeignAPI.ts} (88%) diff --git a/src/commands/utils/liveFlights.ts b/src/commands/utils/liveFlights.ts index 574b08e4..c00bc79c 100644 --- a/src/commands/utils/liveFlights.ts +++ b/src/commands/utils/liveFlights.ts @@ -1,7 +1,7 @@ import { ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { slashCommand, slashCommandStructure, makeEmbed, Logger, fetchData, TelexCountSchema } from '../../lib'; +import { slashCommand, slashCommandStructure, makeEmbed, Logger, fetchForeignAPI, TelexCountSchema } from '../../lib'; const data = slashCommandStructure({ name: 'live-flights', @@ -16,7 +16,7 @@ export default slashCommand(data, async ({ interaction }) => { await interaction.deferReply(); try { - const flights = await fetchData(new Request(`${FBW_API_BASE_URL}/txcxn/_count`), TelexCountSchema); + const flights = await fetchForeignAPI(new Request(`${FBW_API_BASE_URL}/txcxn/_count`), TelexCountSchema); const flightsEmbed = makeEmbed({ title: 'Live Flights', description: `There are currently **${flights}** active flights with TELEX enabled.`, diff --git a/src/commands/utils/metar.ts b/src/commands/utils/metar.ts index de014134..d7ef48f6 100644 --- a/src/commands/utils/metar.ts +++ b/src/commands/utils/metar.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { constantsConfig, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure, Metar, MetarSchema } from '../../lib'; +import { constantsConfig, fetchForeignAPI, makeEmbed, makeLines, slashCommand, slashCommandStructure, Metar, MetarSchema } from '../../lib'; const data = slashCommandStructure({ name: 'metar', @@ -41,7 +41,7 @@ export default slashCommand(data, async ({ interaction }) => { let metar: Metar; try { - metar = await fetchData(new Request(`https://avwx.rest/api/metar/${icao}`, { + metar = await fetchForeignAPI(new Request(`https://avwx.rest/api/metar/${icao}`, { method: 'GET', headers: { Authorization: metarToken }, }), MetarSchema); diff --git a/src/commands/utils/simbriefData.ts b/src/commands/utils/simbriefData.ts index 0a31e074..3f3a5e34 100644 --- a/src/commands/utils/simbriefData.ts +++ b/src/commands/utils/simbriefData.ts @@ -2,7 +2,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'di import moment from 'moment'; import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { slashCommand, makeEmbed, makeLines, slashCommandStructure, SimbriefFlightPlan, fetchData, SimbriefFlightPlanSchema } from '../../lib'; +import { slashCommand, makeEmbed, makeLines, slashCommandStructure, SimbriefFlightPlan, fetchForeignAPI, SimbriefFlightPlanSchema } from '../../lib'; const data = slashCommandStructure({ name: 'simbrief-data', @@ -81,7 +81,7 @@ export default slashCommand(data, async ({ interaction }) => { let flightplan: SimbriefFlightPlan; try { - flightplan = await fetchData(new Request(`https://www.simbrief.com/api/xml.fetcher.php?json=1&userid=${simbriefId}&username=${simbriefId}`), SimbriefFlightPlanSchema); + flightplan = await fetchForeignAPI(new Request(`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.')] }); diff --git a/src/commands/utils/station.ts b/src/commands/utils/station.ts index 5965391d..735093fd 100644 --- a/src/commands/utils/station.ts +++ b/src/commands/utils/station.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { z, ZodError } from 'zod'; -import { AVWXRunwaySchema, AVWXStation, AVWXStationSchema, fetchData, Logger, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; +import { AVWXRunwaySchema, AVWXStation, AVWXStationSchema, fetchForeignAPI, Logger, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; type Runway = z.infer; @@ -51,7 +51,7 @@ export default slashCommand(data, async ({ interaction }) => { let station: AVWXStation; try { - station = await fetchData(new Request(`https://avwx.rest/api/station/${icao}`, { + station = await fetchForeignAPI(new Request(`https://avwx.rest/api/station/${icao}`, { method: 'GET', headers: { Authorization: stationToken }, }), AVWXStationSchema); diff --git a/src/commands/utils/taf.ts b/src/commands/utils/taf.ts index 417c7d2e..344d6b8c 100644 --- a/src/commands/utils/taf.ts +++ b/src/commands/utils/taf.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { Logger, TAF, TafSchema, fetchData, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; +import { Logger, TAF, TafSchema, fetchForeignAPI, makeEmbed, makeLines, slashCommand, slashCommandStructure } from '../../lib'; const data = slashCommandStructure({ name: 'taf', @@ -51,7 +51,7 @@ export default slashCommand(data, async ({ interaction }) => { let taf: TAF; try { - taf = await fetchData(new Request(`https://avwx.rest/api/taf/${icao}`, { + taf = await fetchForeignAPI(new Request(`https://avwx.rest/api/taf/${icao}`, { method: 'GET', headers: { Authorization: tafToken }, }), TafSchema); diff --git a/src/commands/utils/vatsim/functions/vatsimEvents.ts b/src/commands/utils/vatsim/functions/vatsimEvents.ts index 45161a74..6d3d22de 100644 --- a/src/commands/utils/vatsim/functions/vatsimEvents.ts +++ b/src/commands/utils/vatsim/functions/vatsimEvents.ts @@ -1,6 +1,6 @@ import { ChatInputCommandInteraction, Colors, EmbedField } from 'discord.js'; import { Request } from 'node-fetch'; -import { Logger, VatsimEvents, VatsimEventsSchema, fetchData, makeEmbed } from '../../../../lib'; +import { Logger, VatsimEvents, VatsimEventsSchema, fetchForeignAPI, makeEmbed } from '../../../../lib'; const BASE_VATSIM_URL = 'https://my.vatsim.net'; @@ -17,7 +17,7 @@ const handleLocaleDateString = (date: Date) => date.toLocaleDateString('en-US', export async function handleVatsimEvents(interaction: ChatInputCommandInteraction<'cached'>) { try { - const response = await fetchData(new Request(`${BASE_VATSIM_URL}/api/v1/events/all`), VatsimEventsSchema); + const response = await fetchForeignAPI(new Request(`${BASE_VATSIM_URL}/api/v1/events/all`), VatsimEventsSchema); const filteredEvents = response.data.filter((event) => event.type === 'Event'); const finalList = filteredEvents.slice(0, 5); diff --git a/src/commands/utils/vatsim/vatsim.ts b/src/commands/utils/vatsim/vatsim.ts index 6505be1f..c16c8892 100644 --- a/src/commands/utils/vatsim/vatsim.ts +++ b/src/commands/utils/vatsim/vatsim.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { Logger, VatsimData, VatsimDataSchema, fetchData, makeEmbed, slashCommand, slashCommandStructure } from '../../../lib'; +import { Logger, VatsimData, VatsimDataSchema, fetchForeignAPI, makeEmbed, slashCommand, slashCommandStructure } from '../../../lib'; import { handleVatsimControllers } from './functions/vatsimControllers'; import { handleVatsimEvents } from './functions/vatsimEvents'; import { handleVatsimObservers } from './functions/vatsimObservers'; @@ -92,7 +92,7 @@ export default slashCommand(data, async ({ interaction }) => { // Fetch VATSIM data let vatsimData: VatsimData; try { - vatsimData = await fetchData(new Request('https://data.vatsim.net/v3/vatsim-data.json'), VatsimDataSchema); + vatsimData = await fetchForeignAPI(new Request('https://data.vatsim.net/v3/vatsim-data.json'), VatsimDataSchema); } catch (e) { if (e instanceof ZodError) { e.issues.forEach((issue) => Logger.error(`[zod Issue VATSIM Data] Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); diff --git a/src/commands/utils/wolframAlpha.ts b/src/commands/utils/wolframAlpha.ts index 9e95e063..c4d3de1d 100644 --- a/src/commands/utils/wolframAlpha.ts +++ b/src/commands/utils/wolframAlpha.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { z, ZodError } from 'zod'; -import { fetchData, Logger, makeEmbed, makeLines, slashCommand, slashCommandStructure, WolframAlphaData, WolframAlphaDataSchema, WolframAlphaPodSchema, WolframAlphaSubpodSchema } from '../../lib'; +import { fetchForeignAPI, Logger, makeEmbed, makeLines, slashCommand, slashCommandStructure, WolframAlphaData, WolframAlphaDataSchema, WolframAlphaPodSchema, WolframAlphaSubpodSchema } from '../../lib'; type Pod = z.infer; type Subpod = z.infer; @@ -62,7 +62,7 @@ export default slashCommand(data, async ({ interaction }) => { let response: WolframAlphaData; try { - response = await fetchData(new Request(`${WOLFRAMALPHA_API_URL}${searchParams.toString()}`), WolframAlphaDataSchema); + response = await fetchForeignAPI(new Request(`${WOLFRAMALPHA_API_URL}${searchParams.toString()}`), WolframAlphaDataSchema); } catch (e) { if (e instanceof ZodError) { return interaction.editReply({ embeds: [errorEmbed('Wolfram Alpha returned unknown data.')] }); diff --git a/src/lib/apis/fetchData.ts b/src/lib/apis/fetchForeignAPI.ts similarity index 88% rename from src/lib/apis/fetchData.ts rename to src/lib/apis/fetchForeignAPI.ts index 8aaa00fd..34d764ab 100644 --- a/src/lib/apis/fetchData.ts +++ b/src/lib/apis/fetchForeignAPI.ts @@ -9,7 +9,7 @@ import { Logger } from '../logger'; * @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). */ -export const fetchData = async (request: Request, zodSchema: ZodSchema): Promise => { +export const fetchForeignAPI = async (request: Request, zodSchema: ZodSchema): Promise => { let response: Response; try { response = await fetch(request); @@ -32,8 +32,8 @@ export const fetchData = async (request: Request, zodSchem if (!result.success) { Logger.error('[zod] Data validation failed:'); + console.log(data); // winston doesn't correctly print object at the moment result.error.issues.forEach((issue) => Logger.error(`Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); - console.log(data); throw result.error; } diff --git a/src/lib/index.ts b/src/lib/index.ts index 66854ddf..25a86e3c 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -23,7 +23,7 @@ export * from './schedulerJobs/sendHeartbeat'; export * from './schedulerJobs/postBirthdays'; // API Wrapper -export * from './apis/fetchData'; +export * from './apis/fetchForeignAPI'; export * from './apis/zodSchemas/vatsim/vatsimEventsSchemas'; export * from './apis/zodSchemas/vatsim/vatsimDataSchemas'; export * from './apis/zodSchemas/avwx/metarSchemas'; From 03adb0d4d5b49dcedd986a2f80066c593c7cf035 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 03:47:14 +0200 Subject: [PATCH 27/34] improve fetchForeignAPI zod error logging --- src/lib/apis/fetchForeignAPI.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/lib/apis/fetchForeignAPI.ts b/src/lib/apis/fetchForeignAPI.ts index 34d764ab..a4e1d649 100644 --- a/src/lib/apis/fetchForeignAPI.ts +++ b/src/lib/apis/fetchForeignAPI.ts @@ -9,7 +9,7 @@ import { Logger } from '../logger'; * @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). */ -export const fetchForeignAPI = async (request: Request, zodSchema: ZodSchema): Promise => { +export const fetchForeignAPI = async (request: Request, zodSchema: ZodSchema, debug?: boolean): Promise => { let response: Response; try { response = await fetch(request); @@ -31,9 +31,14 @@ export const fetchForeignAPI = async (request: Request, zo const result = zodSchema.safeParse(data); if (!result.success) { - Logger.error('[zod] Data validation failed:'); - console.log(data); // winston doesn't correctly print object at the moment - result.error.issues.forEach((issue) => Logger.error(`Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); + Logger.error("[zod] Data validation failed! Pass the 'debug' flag to 'fetchForeignAPI()' to print the retrieved data to the console."); + Logger.error(`Endpoint location: ${request.url}.`); + if (debug) { + // winston doesn't usefully log object at the moment + // eslint-disable-next-line no-console + console.log('RETRIEVED DATA:', data); + } + result.error.issues.forEach((issue) => Logger.error(`[zod] Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); throw result.error; } From c01ad7a0af2ccd0d6e80c826746b83fced8a7aa3 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 03:51:09 +0200 Subject: [PATCH 28/34] allow string and url when fetching from foreign apis --- src/lib/apis/fetchForeignAPI.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib/apis/fetchForeignAPI.ts b/src/lib/apis/fetchForeignAPI.ts index a4e1d649..d26e2e28 100644 --- a/src/lib/apis/fetchForeignAPI.ts +++ b/src/lib/apis/fetchForeignAPI.ts @@ -1,4 +1,4 @@ -import fetch, { Request, Response } from 'node-fetch'; +import fetch, { Request, RequestInfo, Response } from 'node-fetch'; import { ZodSchema } from 'zod'; import { Logger } from '../logger'; @@ -9,12 +9,14 @@ import { Logger } from '../logger'; * @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). */ -export const fetchForeignAPI = async (request: Request, zodSchema: ZodSchema, debug?: boolean): Promise => { +export const fetchForeignAPI = async (request: RequestInfo, zodSchema: ZodSchema, debug?: boolean): Promise => { + const req = new Request(request); + let response: Response; try { - response = await fetch(request); + response = await fetch(req); } catch (e) { - throw new Error(`An error occured while fetching data from ${request.url}: ${String(e)}`); + throw new Error(`An error occured while fetching data from ${req.url}: ${String(e)}`); } if (!response.ok) { @@ -25,14 +27,14 @@ export const fetchForeignAPI = async (request: Request, zo try { data = await response.json(); } catch (e) { - throw new Error(`Could not parse JSON. Make sure the endpoint at ${request.url} returns valid JSON. Error: ${String(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 print the retrieved data to the console."); - Logger.error(`Endpoint location: ${request.url}.`); + Logger.error(`Endpoint location: ${req.url}.`); if (debug) { // winston doesn't usefully log object at the moment // eslint-disable-next-line no-console From 6647032eb976dee458da4a35f632be41b4ead31b Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 03:55:42 +0200 Subject: [PATCH 29/34] use string instead of request object for foreign apis --- src/commands/utils/liveFlights.ts | 3 +-- src/commands/utils/simbriefData.ts | 5 ++--- src/commands/utils/vatsim/functions/vatsimEvents.ts | 3 +-- src/commands/utils/vatsim/vatsim.ts | 3 +-- src/commands/utils/wolframAlpha.ts | 3 +-- 5 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/commands/utils/liveFlights.ts b/src/commands/utils/liveFlights.ts index c00bc79c..8d4e3844 100644 --- a/src/commands/utils/liveFlights.ts +++ b/src/commands/utils/liveFlights.ts @@ -1,5 +1,4 @@ import { ApplicationCommandType, Colors } from 'discord.js'; -import { Request } from 'node-fetch'; import { ZodError } from 'zod'; import { slashCommand, slashCommandStructure, makeEmbed, Logger, fetchForeignAPI, TelexCountSchema } from '../../lib'; @@ -16,7 +15,7 @@ export default slashCommand(data, async ({ interaction }) => { await interaction.deferReply(); try { - const flights = await fetchForeignAPI(new Request(`${FBW_API_BASE_URL}/txcxn/_count`), TelexCountSchema); + 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.`, diff --git a/src/commands/utils/simbriefData.ts b/src/commands/utils/simbriefData.ts index 3f3a5e34..c7519981 100644 --- a/src/commands/utils/simbriefData.ts +++ b/src/commands/utils/simbriefData.ts @@ -1,8 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import moment from 'moment'; -import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { slashCommand, makeEmbed, makeLines, slashCommandStructure, SimbriefFlightPlan, fetchForeignAPI, SimbriefFlightPlanSchema } from '../../lib'; +import { fetchForeignAPI, makeEmbed, makeLines, SimbriefFlightPlan, SimbriefFlightPlanSchema, slashCommand, slashCommandStructure } from '../../lib'; const data = slashCommandStructure({ name: 'simbrief-data', @@ -81,7 +80,7 @@ export default slashCommand(data, async ({ interaction }) => { let flightplan: SimbriefFlightPlan; try { - flightplan = await fetchForeignAPI(new Request(`https://www.simbrief.com/api/xml.fetcher.php?json=1&userid=${simbriefId}&username=${simbriefId}`), SimbriefFlightPlanSchema); + 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.')] }); diff --git a/src/commands/utils/vatsim/functions/vatsimEvents.ts b/src/commands/utils/vatsim/functions/vatsimEvents.ts index 6d3d22de..0cac5ce6 100644 --- a/src/commands/utils/vatsim/functions/vatsimEvents.ts +++ b/src/commands/utils/vatsim/functions/vatsimEvents.ts @@ -1,5 +1,4 @@ import { ChatInputCommandInteraction, Colors, EmbedField } from 'discord.js'; -import { Request } from 'node-fetch'; import { Logger, VatsimEvents, VatsimEventsSchema, fetchForeignAPI, makeEmbed } from '../../../../lib'; const BASE_VATSIM_URL = 'https://my.vatsim.net'; @@ -17,7 +16,7 @@ const handleLocaleDateString = (date: Date) => date.toLocaleDateString('en-US', export async function handleVatsimEvents(interaction: ChatInputCommandInteraction<'cached'>) { try { - const response = await fetchForeignAPI(new Request(`${BASE_VATSIM_URL}/api/v1/events/all`), VatsimEventsSchema); + 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); diff --git a/src/commands/utils/vatsim/vatsim.ts b/src/commands/utils/vatsim/vatsim.ts index c16c8892..bb304d17 100644 --- a/src/commands/utils/vatsim/vatsim.ts +++ b/src/commands/utils/vatsim/vatsim.ts @@ -1,5 +1,4 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; -import { Request } from 'node-fetch'; import { ZodError } from 'zod'; import { Logger, VatsimData, VatsimDataSchema, fetchForeignAPI, makeEmbed, slashCommand, slashCommandStructure } from '../../../lib'; import { handleVatsimControllers } from './functions/vatsimControllers'; @@ -92,7 +91,7 @@ export default slashCommand(data, async ({ interaction }) => { // Fetch VATSIM data let vatsimData: VatsimData; try { - vatsimData = await fetchForeignAPI(new Request('https://data.vatsim.net/v3/vatsim-data.json'), VatsimDataSchema); + vatsimData = await fetchForeignAPI('https://data.vatsim.net/v3/vatsim-data.json', VatsimDataSchema); } catch (e) { if (e instanceof ZodError) { e.issues.forEach((issue) => Logger.error(`[zod Issue VATSIM Data] Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); diff --git a/src/commands/utils/wolframAlpha.ts b/src/commands/utils/wolframAlpha.ts index c4d3de1d..e8cfcf12 100644 --- a/src/commands/utils/wolframAlpha.ts +++ b/src/commands/utils/wolframAlpha.ts @@ -1,5 +1,4 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; -import { Request } from 'node-fetch'; import { z, ZodError } from 'zod'; import { fetchForeignAPI, Logger, makeEmbed, makeLines, slashCommand, slashCommandStructure, WolframAlphaData, WolframAlphaDataSchema, WolframAlphaPodSchema, WolframAlphaSubpodSchema } from '../../lib'; @@ -62,7 +61,7 @@ export default slashCommand(data, async ({ interaction }) => { let response: WolframAlphaData; try { - response = await fetchForeignAPI(new Request(`${WOLFRAMALPHA_API_URL}${searchParams.toString()}`), WolframAlphaDataSchema); + 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.')] }); From 0b2de9ea2bdb7fbc2aff8394a5401841820b83c9 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Sun, 28 Jul 2024 17:46:00 +0200 Subject: [PATCH 30/34] add changelog # Conflicts: # .github/CHANGELOG.md --- .github/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 75ab3e2d..85dd9337 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -8,6 +8,7 @@ Update _ August 2024 Update _ July 2024 +- feat: generic wrapper for JSON APIs (CHANGE DATE) - fix: resolve security vulnerabilities in 3rd-party packages (27/07/2024) - fix: corrected typo in the embed for simbrief-data support-request (27/07/2024) - docs: updated the Ground Rules on the Contributing guide page (27/07/2024) From a5b363ede88b6627376f43c3873762383b204ee4 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:55:03 +0200 Subject: [PATCH 31/34] self review --- src/commands/utils/metar.ts | 5 ++-- src/commands/utils/simbriefData.ts | 7 +++--- src/commands/utils/station.ts | 2 +- src/commands/utils/taf.ts | 2 +- .../utils/vatsim/functions/vatsimEvents.ts | 6 ++--- .../utils/vatsim/functions/vatsimPilots.ts | 4 ++-- src/commands/utils/vatsim/vatsim.ts | 6 ++--- src/commands/utils/wolframAlpha.ts | 6 ++--- src/lib/apis/fetchForeignAPI.ts | 6 ++--- .../zodSchemas/simbrief/simbriefSchemas.ts | 24 +++++++++---------- 10 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/commands/utils/metar.ts b/src/commands/utils/metar.ts index d7ef48f6..2cc42e3f 100644 --- a/src/commands/utils/metar.ts +++ b/src/commands/utils/metar.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import { Request } from 'node-fetch'; import { ZodError } from 'zod'; -import { constantsConfig, fetchForeignAPI, makeEmbed, makeLines, slashCommand, slashCommandStructure, Metar, MetarSchema } from '../../lib'; +import { constantsConfig, fetchForeignAPI, makeEmbed, makeLines, slashCommand, slashCommandStructure, Metar, MetarSchema, Logger } from '../../lib'; const data = slashCommandStructure({ name: 'metar', @@ -41,7 +41,7 @@ export default slashCommand(data, async ({ interaction }) => { let metar: Metar; try { - metar = await fetchForeignAPI(new Request(`https://avwx.rest/api/metar/${icao}`, { + metar = await fetchForeignAPI(new Request(`https://avwx.rest/api/metar/${icao}`, { method: 'GET', headers: { Authorization: metarToken }, }), MetarSchema); @@ -49,6 +49,7 @@ export default slashCommand(data, async ({ interaction }) => { 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()}.`)] }); } diff --git a/src/commands/utils/simbriefData.ts b/src/commands/utils/simbriefData.ts index c7519981..4fb47dd6 100644 --- a/src/commands/utils/simbriefData.ts +++ b/src/commands/utils/simbriefData.ts @@ -1,7 +1,7 @@ import { ApplicationCommandOptionType, ApplicationCommandType, Colors } from 'discord.js'; import moment from 'moment'; import { ZodError } from 'zod'; -import { fetchForeignAPI, makeEmbed, makeLines, SimbriefFlightPlan, SimbriefFlightPlanSchema, slashCommand, slashCommandStructure } from '../../lib'; +import { fetchForeignAPI, Logger, makeEmbed, makeLines, SimbriefFlightPlan, SimbriefFlightPlanSchema, slashCommand, slashCommandStructure } from '../../lib'; const data = slashCommandStructure({ name: 'simbrief-data', @@ -41,9 +41,9 @@ const simbriefdatarequestEmbed = makeEmbed({ ]), }); -const errorEmbed = (errorMessage: string) => makeEmbed({ +const errorEmbed = (error: string) => makeEmbed({ title: 'SimBrief Error', - description: errorMessage, + description: error, color: Colors.Red, }); @@ -85,6 +85,7 @@ export default slashCommand(data, async ({ interaction }) => { 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.')] }); } diff --git a/src/commands/utils/station.ts b/src/commands/utils/station.ts index 735093fd..20275e08 100644 --- a/src/commands/utils/station.ts +++ b/src/commands/utils/station.ts @@ -51,7 +51,7 @@ export default slashCommand(data, async ({ interaction }) => { let station: AVWXStation; try { - station = await fetchForeignAPI(new Request(`https://avwx.rest/api/station/${icao}`, { + station = await fetchForeignAPI(new Request(`https://avwx.rest/api/station/${icao}`, { method: 'GET', headers: { Authorization: stationToken }, }), AVWXStationSchema); diff --git a/src/commands/utils/taf.ts b/src/commands/utils/taf.ts index 344d6b8c..96caab98 100644 --- a/src/commands/utils/taf.ts +++ b/src/commands/utils/taf.ts @@ -51,7 +51,7 @@ export default slashCommand(data, async ({ interaction }) => { let taf: TAF; try { - taf = await fetchForeignAPI(new Request(`https://avwx.rest/api/taf/${icao}`, { + taf = await fetchForeignAPI(new Request(`https://avwx.rest/api/taf/${icao}`, { method: 'GET', headers: { Authorization: tafToken }, }), TafSchema); diff --git a/src/commands/utils/vatsim/functions/vatsimEvents.ts b/src/commands/utils/vatsim/functions/vatsimEvents.ts index 0cac5ce6..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, VatsimEvents, VatsimEventsSchema, fetchForeignAPI, makeEmbed } from '../../../../lib'; +import { Logger, VatsimEventsSchema, fetchForeignAPI, makeEmbed } from '../../../../lib'; const BASE_VATSIM_URL = 'https://my.vatsim.net'; @@ -16,7 +16,7 @@ const handleLocaleDateString = (date: Date) => date.toLocaleDateString('en-US', export async function handleVatsimEvents(interaction: ChatInputCommandInteraction<'cached'>) { try { - const response = await fetchForeignAPI(`${BASE_VATSIM_URL}/api/v1/events/all`, VatsimEventsSchema); + 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); @@ -69,7 +69,7 @@ export async function handleVatsimEvents(interaction: ChatInputCommandInteractio return interaction.editReply({ embeds: [eventsEmbed] }); } catch (e) { - Logger.error(String(e)); + Logger.error(e); const errorEmbed = makeEmbed({ title: 'Events Error', description: String(e), diff --git a/src/commands/utils/vatsim/functions/vatsimPilots.ts b/src/commands/utils/vatsim/functions/vatsimPilots.ts index 31096301..70d9cf1f 100644 --- a/src/commands/utils/vatsim/functions/vatsimPilots.ts +++ b/src/commands/utils/vatsim/functions/vatsimPilots.ts @@ -12,7 +12,7 @@ const listEmbed = (type: string, fields: EmbedField[], totalCount: number, shown description: `A list of ${shownCount} online ${type} matching ${callsign}.`, fields, }); -const pilotsListEmbedFields = (callsign: string, rating?: PilotRating, flightPlan?: FlightPlan) => { +const pilotsListEmbedFields = (callsign: string, flightPlan: FlightPlan | null, rating?: PilotRating) => { const fields = [ { name: 'Callsign', @@ -56,7 +56,7 @@ export async function handleVatsimPilots(interaction: ChatInputCommandInteractio const { callsign, flight_plan } = pilot; const rating = vatsimData.pilot_ratings.find((rating) => rating.id === pilot.pilot_rating); - return pilotsListEmbedFields(callsign, rating, flight_plan ?? undefined); + return pilotsListEmbedFields(callsign, flight_plan, rating); }).splice(0, 5); return interaction.editReply({ embeds: [listEmbed('Pilots', fields.flat(), pilots.length, fields.length, callsignSearch)] }); diff --git a/src/commands/utils/vatsim/vatsim.ts b/src/commands/utils/vatsim/vatsim.ts index bb304d17..d3dfbf66 100644 --- a/src/commands/utils/vatsim/vatsim.ts +++ b/src/commands/utils/vatsim/vatsim.ts @@ -91,13 +91,13 @@ export default slashCommand(data, async ({ interaction }) => { // Fetch VATSIM data let vatsimData: VatsimData; try { - vatsimData = await fetchForeignAPI('https://data.vatsim.net/v3/vatsim-data.json', VatsimDataSchema); + vatsimData = await fetchForeignAPI('https://data.vatsim.net/v3/vatsim-data.json', VatsimDataSchema); } catch (e) { if (e instanceof ZodError) { - e.issues.forEach((issue) => Logger.error(`[zod Issue VATSIM Data] Code: ${issue.code}, Path: ${issue.path.join('.')}, Message: ${issue.message}`)); return interaction.editReply({ embeds: [fetchErrorEmbed('The VATSIM API returned unknown data.')] }); } - 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 diff --git a/src/commands/utils/wolframAlpha.ts b/src/commands/utils/wolframAlpha.ts index e8cfcf12..c0dbc686 100644 --- a/src/commands/utils/wolframAlpha.ts +++ b/src/commands/utils/wolframAlpha.ts @@ -23,9 +23,9 @@ const noQueryEmbed = makeEmbed({ color: Colors.Red, }); -const errorEmbed = (errorMessage: string) => makeEmbed({ +const errorEmbed = (error: string) => makeEmbed({ title: 'Wolfram Alpha Error', - description: errorMessage, + description: error, color: Colors.Red, }); @@ -61,7 +61,7 @@ export default slashCommand(data, async ({ interaction }) => { let response: WolframAlphaData; try { - response = await fetchForeignAPI(`${WOLFRAMALPHA_API_URL}${searchParams.toString()}`, WolframAlphaDataSchema); + 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.')] }); diff --git a/src/lib/apis/fetchForeignAPI.ts b/src/lib/apis/fetchForeignAPI.ts index d26e2e28..8ae1ef8d 100644 --- a/src/lib/apis/fetchForeignAPI.ts +++ b/src/lib/apis/fetchForeignAPI.ts @@ -7,7 +7,7 @@ import { Logger } from '../logger'; * @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). + * @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); @@ -33,10 +33,10 @@ export const fetchForeignAPI = async (request: RequestInfo const result = zodSchema.safeParse(data); if (!result.success) { - Logger.error("[zod] Data validation failed! Pass the 'debug' flag to 'fetchForeignAPI()' to print the retrieved data to the console."); + 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) { - // winston doesn't usefully log object at the moment + // winston doesn't log objects in a useful way at the moment // eslint-disable-next-line no-console console.log('RETRIEVED DATA:', data); } diff --git a/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts b/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts index 07b71b4a..eb058a25 100644 --- a/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts +++ b/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts @@ -1,34 +1,34 @@ import { z } from 'zod'; -const FetchSchema = z.object({ status: z.string() }); +const SimBriefFetchSchema = z.object({ status: z.string() }); -const ParamsSchema = z.object({ +const SimBriefParamsSchema = z.object({ user_id: z.string(), time_generated: z.string(), airac: z.string(), }); -const AircraftSchema = z.object({ +const SimBriefAircraftSchema = z.object({ name: z.string(), internal_id: z.string(), }); -const OriginSchema = z.object({ icao_code: z.string() }); +const SimBriefOriginSchema = z.object({ icao_code: z.string() }); -const DestinationSchema = z.object({ icao_code: z.string() }); +const SimBriefDestinationSchema = z.object({ icao_code: z.string() }); -const GeneralSchema = z.object({ route: 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: FetchSchema, - params: ParamsSchema, - aircraft: AircraftSchema, - origin: OriginSchema, - destination: DestinationSchema, - general: GeneralSchema, + fetch: SimBriefFetchSchema, + params: SimBriefParamsSchema, + aircraft: SimBriefAircraftSchema, + origin: SimBriefOriginSchema, + destination: SimBriefDestinationSchema, + general: SimBriefGeneralSchema, }); /** From aa84fa90158e610dc7b5930097f5f4cccb697794 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Thu, 8 Aug 2024 19:21:29 +0200 Subject: [PATCH 32/34] update simbrief zod schema --- src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts b/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts index eb058a25..3b1b77ea 100644 --- a/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts +++ b/src/lib/apis/zodSchemas/simbrief/simbriefSchemas.ts @@ -13,9 +13,15 @@ const SimBriefAircraftSchema = z.object({ internal_id: z.string(), }); -const SimBriefOriginSchema = z.object({ icao_code: z.string() }); +const SimBriefOriginSchema = z.object({ + icao_code: z.string(), + plan_rwy: z.string(), +}); -const SimBriefDestinationSchema = z.object({ icao_code: z.string() }); +const SimBriefDestinationSchema = z.object({ + icao_code: z.string(), + plan_rwy: z.string(), +}); const SimBriefGeneralSchema = z.object({ route: z.string() }); From 7e46e6b00b0b16ed39416a6f9c47417d2c5a7dbf Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:44:39 +0200 Subject: [PATCH 33/34] remove console --- src/lib/apis/fetchForeignAPI.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/apis/fetchForeignAPI.ts b/src/lib/apis/fetchForeignAPI.ts index 8ae1ef8d..f6625cf3 100644 --- a/src/lib/apis/fetchForeignAPI.ts +++ b/src/lib/apis/fetchForeignAPI.ts @@ -36,9 +36,7 @@ export const fetchForeignAPI = async (request: RequestInfo 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) { - // winston doesn't log objects in a useful way at the moment - // eslint-disable-next-line no-console - console.log('RETRIEVED DATA:', data); + 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; From 256b6db158e2e6abed9189cbd04ed0dc2ceeec13 Mon Sep 17 00:00:00 2001 From: ExampleWasTaken <58574351+ExampleWasTaken@users.noreply.github.com> Date: Sun, 27 Oct 2024 21:16:48 +0100 Subject: [PATCH 34/34] update changelog --- .github/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 13c73c60..5028df28 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,7 @@ Update _ October 2024 +- feat: generic wrapper for JSON APIs (27/10/2024) - fix: role assignment typo for server announcements (22/10/2024) Update _ August 2024 @@ -12,7 +13,6 @@ Update _ August 2024 Update _ July 2024 -- feat: generic wrapper for JSON APIs (CHANGE DATE) - fix: resolve security vulnerabilities in 3rd-party packages (27/07/2024) - fix: corrected typo in the embed for simbrief-data support-request (27/07/2024) - docs: updated the Ground Rules on the Contributing guide page (27/07/2024)