From 93b261c2a889a24a477328798b9b563b835fb3c6 Mon Sep 17 00:00:00 2001 From: Yankai Zhu Date: Mon, 16 Jun 2025 18:39:40 +0800 Subject: [PATCH 1/3] Added backend events API --- .gitignore | 2 + backend/src/controllers/events.ts | 6 ++ backend/src/controllers/eventsWebhook.ts | 107 +++++++++++++++++++ backend/src/data/eventData.ts | 126 +++++++++++++++++++++++ backend/src/index.ts | 43 ++++++-- backend/src/routes/events.ts | 8 ++ backend/src/routes/eventsWebhook.ts | 9 ++ backend/src/types/express/index.d.ts | 11 ++ backend/src/types/http/index.d.ts | 7 ++ backend/tsconfig.json | 2 +- 10 files changed, 309 insertions(+), 12 deletions(-) create mode 100644 backend/src/controllers/events.ts create mode 100644 backend/src/controllers/eventsWebhook.ts create mode 100644 backend/src/data/eventData.ts create mode 100644 backend/src/routes/events.ts create mode 100644 backend/src/routes/eventsWebhook.ts create mode 100644 backend/src/types/express/index.d.ts create mode 100644 backend/src/types/http/index.d.ts diff --git a/.gitignore b/.gitignore index 8d2da39..a437c64 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +*.env \ No newline at end of file diff --git a/backend/src/controllers/events.ts b/backend/src/controllers/events.ts new file mode 100644 index 0000000..6ca0d96 --- /dev/null +++ b/backend/src/controllers/events.ts @@ -0,0 +1,6 @@ +import { RequestHandler } from "express"; +import { eventInfo } from "../data/eventData"; + +export const EventsHandler: RequestHandler = (req, res) => { + res.status(200).json(eventInfo); +} \ No newline at end of file diff --git a/backend/src/controllers/eventsWebhook.ts b/backend/src/controllers/eventsWebhook.ts new file mode 100644 index 0000000..a09bbd9 --- /dev/null +++ b/backend/src/controllers/eventsWebhook.ts @@ -0,0 +1,107 @@ +import crypto from 'crypto'; +import { RequestHandler } from "express"; +import { eventInfo, fetchEvent, filterInPlace, replaceInPlace } from '../data/eventData'; + +interface ChangesEntry { + field: string; + value: { + event_id: string; + item: string; + verb: string; + } +} + +interface FacebookWebhookNotificationEntry { + id: string; + changes: ChangesEntry[]; +} + +interface FacebookWebhookNotification { + entry: FacebookWebhookNotificationEntry[]; + object: string; +} + +const verifySignature = (rawBody: Buffer, signatureHeader?: string): boolean => { + if (!signatureHeader) return false; + const [algo, signature] = signatureHeader.split('='); + if (algo !== 'sha256') return false; + + const expected = crypto + .createHmac('sha256', process.env.FB_APP_SECRET as string) + .update(rawBody) + .digest('hex'); + + return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); +} + +export const EventsWebhookVerifier: RequestHandler = (req, res) => { + const mode = req.query["hub.mode"]; + const token = req.query["hub.verify_token"]; + const challenge = req.query["hub.challenge"]; + + if (mode === "subscribe" && token === process.env.FB_WEBHOOK_VERIFY_TOKEN) { + return res.status(200).send(challenge); + } + + res.sendStatus(403); +}; + +/* +Sample webhook payload +https://developers.facebook.com/docs/graph-api/webhooks/getting-started/webhooks-for-pages -- for the outer wrapper +https://developers.facebook.com/docs/graph-api/webhooks/reference/page/#feed -- for the inner objects + +{ + "object": "page", + "entry": [ + { + "id": "PAGE_ID", + "time": 1623242342342, + "changes": [ + { + "field": "events", + "value": { + "event_id": "123456789", + "verb": "create", // also "edit" or "delete" + "published": 1 + } + } + ] + } + ] +} +*/ + +export const EventsWebhookUpdate: RequestHandler = async (req, res) => { + const signature = req.headers['x-hub-signature-256']; + if (!req.rawBody || typeof signature !== "string" || !verifySignature(req.rawBody, signature)) { + return res.sendStatus(401); + } + + const notif: FacebookWebhookNotification = req.body; + if (!notif || !notif.entry || notif.object !== "page" || notif.entry.length === 0) { + return res.sendStatus(400); + } + + for (const entry of notif.entry) { + if (entry.id !== process.env.FB_EVENT_PAGE_ID) continue; + + for (const change of entry.changes) { + if (change.field !== "feed" || change.value.item !== "event") continue; + + if (change.value.verb === "delete") { + // we need filter *in place* because all imports are immutable (the REAL const) + filterInPlace(eventInfo, (val, index, arr) => val.id !== change.value.event_id); + } else { + try { + const newEvent = await fetchEvent(change.value.event_id); + replaceInPlace(eventInfo, (val, index, arr) => val.id === change.value.event_id, newEvent); + } catch(err) { + console.log(`Wasn't able to update event for some reason: ${err}`); + } + } + } + } + + res.sendStatus(200); +} \ No newline at end of file diff --git a/backend/src/data/eventData.ts b/backend/src/data/eventData.ts new file mode 100644 index 0000000..56d6423 --- /dev/null +++ b/backend/src/data/eventData.ts @@ -0,0 +1,126 @@ +class EventInfo { + // god forbid a class have public members + public id: string; + public title: string; + public startTime: string; + public endTime?: string; + public location: string; + public imageUrl: string; + public link: string; + + constructor( + id: string, + title: string, + startTime: string, + endTime: string | undefined, + location: string, + imageUrl: string + ) { + this.id = id; + this.title = title; + this.startTime = startTime; + this.endTime = endTime; + this.location = location; + this.imageUrl = imageUrl; + // would use link as getter but getters are not enumerable so it doesn't appear in JSON.stringify :skull: + // maybe a cursed fix would be to use Object.defineProperty LOL + this.link = `https://www.facebook.com/events/${id}`; + } +} + +// We are altering the array in place, pray we do not alter it from another thread +// I don't even know if concurrent modification exception is a thing in JS +// Maybe this is a single threaded moment :icant: +export function filterInPlace( + arr: T[], + predicate: (value: T, index: number, array: T[]) => boolean +): T[] { + let write = 0; + for (let read = 0; read < arr.length; read++) { + const val = arr[read]; + if (predicate(val, read, arr)) { + arr[write++] = val; + } + } + arr.length = write; + return arr; +} + +// This one is definitely not thread safe lmao +// TODO fix with a mutex probably +export function replaceInPlace( + arr: T[], + predicate: (value: T, index: number, array: T[]) => boolean, + replacement: T +): number { + const idx = arr.findIndex(predicate); + if (idx !== -1) arr[idx] = replacement; + return idx; +} + +// we LOVE global variables +export let eventInfo: EventInfo[] = []; + +interface FacebookEvent { + id: string; + name: string; + cover?: { source: string }; + place?: { name: string }; + start_time: string; + end_time?: string; +} + +interface FacebookEventsResponse { + data: FacebookEvent[]; +} + +// this isn't in .env for different module compatiblity +const FB_API_VERSION = "v23.0"; + +export async function fetchEvents() { + const response = await fetch( + `https://graph.facebook.com/${FB_API_VERSION}/${process.env.FB_EVENT_PAGE_ID}/events?access_token=${process.env.FB_ACCESS_TOKEN}&fields=id,name,cover,place,start_time,end_time` + ); + + const res: FacebookEventsResponse = await response.json(); + + if (!res || !res.data) { + console.log("No events found..."); + return; + } + + const processed = res.data.map( + (e) => + new EventInfo( + e.id, + e.name, + e.start_time, + e.end_time, + e.place?.name ?? "Everything everywhere all at once!!!", + e.cover?.source || "/images/events/default_event.jpg" + ) + ); + + eventInfo = processed; +} + +export async function fetchEvent(id: string) { + const response = await fetch( + `https://graph.facebook.com/${FB_API_VERSION}/${id}?access_token=${process.env.FB_ACCESS_TOKEN}&fields=id,name,cover,place,start_time,end_time` + ); + + const res: FacebookEvent = await response.json(); + + if (!res) { + throw new Error(`Couldn't get details for event ${id}`); + } + + return new EventInfo( + res.id, + res.name, + res.start_time, + res.end_time, + res.place?.name ?? "Everything everywhere all at once!!!", + res.cover?.source || "/images/events/default_event.jpg" + ); +} diff --git a/backend/src/index.ts b/backend/src/index.ts index d1b845a..c754f19 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -2,18 +2,39 @@ import express, { Express } from "express"; import cors from "cors"; import dotenv from "dotenv"; import pingRoute from "./routes/ping"; +import eventsRoute from "./routes/events"; +import eventsWebhookRoute from "./routes/eventsWebhook"; +import { fetchEvents } from "./data/eventData"; dotenv.config(); -const app: Express = express(); -const port = process.env.PORT || 9000; +(async () => { + try { + await fetchEvents(); + console.log("Events fetched successfully"); + } catch (error) { + // do we ungracefully bail out here??? + console.error("Error fetching events:", error); + } -// Middleware -app.use(express.json()); -app.use(cors()); - -app.use(pingRoute); - -app.listen(port, () => { - console.log(`Server successfully started on port ${port}`); -}); \ No newline at end of file + const app: Express = express(); + const port = process.env.PORT || 9000; + + // Middleware + app.use( + express.json({ + verify: (req, res, buf) => { + req.rawBody = buf; + }, + }) + ); + app.use(cors()); + + app.use(pingRoute); + app.use(eventsWebhookRoute); + app.use(eventsRoute); + + app.listen(port, () => { + console.log(`Server successfully started on port ${port}`); + }); +})(); diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts new file mode 100644 index 0000000..10b6f8f --- /dev/null +++ b/backend/src/routes/events.ts @@ -0,0 +1,8 @@ +import { Router } from "express"; +import { EventsHandler } from "../controllers/events"; + +const router = Router(); + +router.get("/events", EventsHandler); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/eventsWebhook.ts b/backend/src/routes/eventsWebhook.ts new file mode 100644 index 0000000..54322cf --- /dev/null +++ b/backend/src/routes/eventsWebhook.ts @@ -0,0 +1,9 @@ +import { Router } from "express"; +import { EventsWebhookUpdate, EventsWebhookVerifier } from "../controllers/eventsWebhook"; + +const router = Router(); + +router.post("/eventsWebhook", EventsWebhookUpdate); +router.get("/eventsWebhook", EventsWebhookVerifier); + +export default router; \ No newline at end of file diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts new file mode 100644 index 0000000..6c2bf07 --- /dev/null +++ b/backend/src/types/express/index.d.ts @@ -0,0 +1,11 @@ +import "express"; + +declare module "express-serve-static-core" { + interface Request { + rawBody?: Buffer; + } + + interface IncomingMessage { + rawBody?: Buffer; + } +} diff --git a/backend/src/types/http/index.d.ts b/backend/src/types/http/index.d.ts new file mode 100644 index 0000000..36dce5a --- /dev/null +++ b/backend/src/types/http/index.d.ts @@ -0,0 +1,7 @@ +import "http"; + +declare module "http" { + interface IncomingMessage { + rawBody?: Buffer; + } +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 6bc9e20..f5a5419 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -31,7 +31,7 @@ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "typeRoots": ["./src/types"], /* Specify multiple folders that act like './node_modules/@types'. */ // "types": [], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ From 36b5791a8b16443c9d1decd10af6d3f0412df6a7 Mon Sep 17 00:00:00 2001 From: Yankai Zhu Date: Mon, 16 Jun 2025 23:24:06 +0800 Subject: [PATCH 2/3] polished code --- backend/package-lock.json | 14 +++ backend/package.json | 1 + backend/src/controllers/eventsWebhook.ts | 104 +++++++++++++++-------- backend/src/data/eventData.ts | 82 +++++++----------- backend/src/index.ts | 14 +-- backend/src/util.ts | 52 ++++++++++++ 6 files changed, 173 insertions(+), 94 deletions(-) create mode 100644 backend/src/util.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index f07f2f9..d96a388 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -8,6 +8,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^20.12.13", + "async-mutex": "^0.5.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", @@ -230,6 +231,14 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/async-mutex": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", + "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1274,6 +1283,11 @@ } } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/backend/package.json b/backend/package.json index c7532f0..473c29b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,6 +9,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/node": "^20.12.13", + "async-mutex": "^0.5.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", diff --git a/backend/src/controllers/eventsWebhook.ts b/backend/src/controllers/eventsWebhook.ts index a09bbd9..f45c768 100644 --- a/backend/src/controllers/eventsWebhook.ts +++ b/backend/src/controllers/eventsWebhook.ts @@ -1,38 +1,38 @@ -import crypto from 'crypto'; +import crypto from "crypto"; import { RequestHandler } from "express"; -import { eventInfo, fetchEvent, filterInPlace, replaceInPlace } from '../data/eventData'; - -interface ChangesEntry { - field: string; - value: { - event_id: string; - item: string; - verb: string; - } -} +import { eventInfo, eventInfoMutex, fetchEvent } from "../data/eventData"; +import { filterInPlace, replaceInPlace } from "../util"; -interface FacebookWebhookNotificationEntry { - id: string; - changes: ChangesEntry[]; -} - -interface FacebookWebhookNotification { - entry: FacebookWebhookNotificationEntry[]; +interface FacebookWebhookPayload { object: string; + entry: Array<{ + id: string; + changes: Array<{ + field: string; + value: { + event_id: string; + item: string; + verb: string; + }; + }>; + }>; } -const verifySignature = (rawBody: Buffer, signatureHeader?: string): boolean => { +const verifySignature = ( + rawBody: Buffer, + signatureHeader?: string +): boolean => { if (!signatureHeader) return false; - const [algo, signature] = signatureHeader.split('='); - if (algo !== 'sha256') return false; + const [algo, signature] = signatureHeader.split("="); + if (algo !== "sha256") return false; const expected = crypto - .createHmac('sha256', process.env.FB_APP_SECRET as string) + .createHmac("sha256", process.env.FB_APP_SECRET as string) .update(rawBody) - .digest('hex'); + .digest("hex"); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); -} +}; export const EventsWebhookVerifier: RequestHandler = (req, res) => { const mode = req.query["hub.mode"]; @@ -73,13 +73,22 @@ https://developers.facebook.com/docs/graph-api/webhooks/reference/page/#feed -- */ export const EventsWebhookUpdate: RequestHandler = async (req, res) => { - const signature = req.headers['x-hub-signature-256']; - if (!req.rawBody || typeof signature !== "string" || !verifySignature(req.rawBody, signature)) { + const signature = req.headers["x-hub-signature-256"]; + if ( + !req.rawBody || + typeof signature !== "string" || + !verifySignature(req.rawBody, signature) + ) { return res.sendStatus(401); } - const notif: FacebookWebhookNotification = req.body; - if (!notif || !notif.entry || notif.object !== "page" || notif.entry.length === 0) { + const notif: FacebookWebhookPayload = req.body; + if ( + !notif || + !notif.entry || + notif.object !== "page" || + notif.entry.length === 0 + ) { return res.sendStatus(400); } @@ -89,19 +98,40 @@ export const EventsWebhookUpdate: RequestHandler = async (req, res) => { for (const change of entry.changes) { if (change.field !== "feed" || change.value.item !== "event") continue; - if (change.value.verb === "delete") { - // we need filter *in place* because all imports are immutable (the REAL const) - filterInPlace(eventInfo, (val, index, arr) => val.id !== change.value.event_id); - } else { - try { + try { + if (change.value.verb === "delete") { + await eventInfoMutex.runExclusive(() => + filterInPlace(eventInfo, (val) => val.id !== change.value.event_id) + ); + console.log(`Deleted event: ${change.value.event_id}`); + } else if (change.value.verb === "edit") { + const newEvent = await fetchEvent(change.value.event_id); + + eventInfoMutex.runExclusive(() => + replaceInPlace( + eventInfo, + (val) => val.id === change.value.event_id, + newEvent + ) + ); + console.log(`Edited event: ${change.value.event_id}`); + } else if (change.value.verb === "add") { const newEvent = await fetchEvent(change.value.event_id); - replaceInPlace(eventInfo, (val, index, arr) => val.id === change.value.event_id, newEvent); - } catch(err) { - console.log(`Wasn't able to update event for some reason: ${err}`); + await eventInfoMutex.runExclusive(() => eventInfo.push(newEvent)); + console.log(`Added event: ${change.value.event_id}`); + } else { + console.warn( + `Unknown verb "${change.value.verb}" for event ${change.value.event_id}` + ); } + } catch (err) { + console.error( + `Error processing event: ${change.value.event_id}:\n${err}` + ); + return res.sendStatus(500); } } } res.sendStatus(200); -} \ No newline at end of file +}; diff --git a/backend/src/data/eventData.ts b/backend/src/data/eventData.ts index 56d6423..6030f39 100644 --- a/backend/src/data/eventData.ts +++ b/backend/src/data/eventData.ts @@ -1,3 +1,7 @@ +import { Mutex } from "async-mutex"; +import { inspect } from "util"; +import { FacebookError, Result, ResultType } from "../util"; + class EventInfo { // god forbid a class have public members public id: string; @@ -28,39 +32,6 @@ class EventInfo { } } -// We are altering the array in place, pray we do not alter it from another thread -// I don't even know if concurrent modification exception is a thing in JS -// Maybe this is a single threaded moment :icant: -export function filterInPlace( - arr: T[], - predicate: (value: T, index: number, array: T[]) => boolean -): T[] { - let write = 0; - for (let read = 0; read < arr.length; read++) { - const val = arr[read]; - if (predicate(val, read, arr)) { - arr[write++] = val; - } - } - arr.length = write; - return arr; -} - -// This one is definitely not thread safe lmao -// TODO fix with a mutex probably -export function replaceInPlace( - arr: T[], - predicate: (value: T, index: number, array: T[]) => boolean, - replacement: T -): number { - const idx = arr.findIndex(predicate); - if (idx !== -1) arr[idx] = replacement; - return idx; -} - -// we LOVE global variables -export let eventInfo: EventInfo[] = []; - interface FacebookEvent { id: string; name: string; @@ -76,32 +47,37 @@ interface FacebookEventsResponse { // this isn't in .env for different module compatiblity const FB_API_VERSION = "v23.0"; +const DEFAULT_EVENT_LOCATION = "Everything everywhere all at once!!!"; +const DEFAULT_EVENT_IMAGE = "/images/events/default_event.jpg"; + +// we LOVE global variables +export const eventInfoMutex = new Mutex(); +export const eventInfo: EventInfo[] = []; export async function fetchEvents() { const response = await fetch( `https://graph.facebook.com/${FB_API_VERSION}/${process.env.FB_EVENT_PAGE_ID}/events?access_token=${process.env.FB_ACCESS_TOKEN}&fields=id,name,cover,place,start_time,end_time` ); - const res: FacebookEventsResponse = await response.json(); - - if (!res || !res.data) { - console.log("No events found..."); - return; + const res: Result = await response.json(); + if (!res || res.type === ResultType.Err) { + console.log(`No events found...\n${res}`); + return []; } - const processed = res.data.map( + const processed = res.value.data.map( (e) => new EventInfo( e.id, e.name, e.start_time, e.end_time, - e.place?.name ?? "Everything everywhere all at once!!!", - e.cover?.source || "/images/events/default_event.jpg" + e.place?.name ?? DEFAULT_EVENT_LOCATION, + e.cover?.source ?? DEFAULT_EVENT_IMAGE ) ); - eventInfo = processed; + return processed; } export async function fetchEvent(id: string) { @@ -109,18 +85,22 @@ export async function fetchEvent(id: string) { `https://graph.facebook.com/${FB_API_VERSION}/${id}?access_token=${process.env.FB_ACCESS_TOKEN}&fields=id,name,cover,place,start_time,end_time` ); - const res: FacebookEvent = await response.json(); + const res: Result = await response.json(); - if (!res) { - throw new Error(`Couldn't get details for event ${id}`); + if (!res || res.type === ResultType.Err) { + throw new Error( + `Couldn't fetch details for event ${id}\n${inspect( + Object.getOwnPropertyDescriptor(res, "error")?.value + )}` + ); } return new EventInfo( - res.id, - res.name, - res.start_time, - res.end_time, - res.place?.name ?? "Everything everywhere all at once!!!", - res.cover?.source || "/images/events/default_event.jpg" + res.value.id, + res.value.name, + res.value.start_time, + res.value.end_time, + res.value.place?.name ?? DEFAULT_EVENT_LOCATION, + res.value.cover?.source ?? DEFAULT_EVENT_IMAGE ); } diff --git a/backend/src/index.ts b/backend/src/index.ts index c754f19..e3a7ae2 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,22 +4,24 @@ import dotenv from "dotenv"; import pingRoute from "./routes/ping"; import eventsRoute from "./routes/events"; import eventsWebhookRoute from "./routes/eventsWebhook"; -import { fetchEvents } from "./data/eventData"; +import { eventInfo, eventInfoMutex, fetchEvents } from "./data/eventData"; dotenv.config(); (async () => { try { - await fetchEvents(); + const events = await fetchEvents(); + eventInfoMutex.runExclusive(() => eventInfo.concat(events)); console.log("Events fetched successfully"); } catch (error) { // do we ungracefully bail out here??? - console.error("Error fetching events:", error); + // could just load from a backup file instead + console.error("Error fetching events on startup:", error); } const app: Express = express(); const port = process.env.PORT || 9000; - + // Middleware app.use( express.json({ @@ -29,11 +31,11 @@ dotenv.config(); }) ); app.use(cors()); - + app.use(pingRoute); app.use(eventsWebhookRoute); app.use(eventsRoute); - + app.listen(port, () => { console.log(`Server successfully started on port ${port}`); }); diff --git a/backend/src/util.ts b/backend/src/util.ts new file mode 100644 index 0000000..9d4df0a --- /dev/null +++ b/backend/src/util.ts @@ -0,0 +1,52 @@ +// These are NOT thread-safe functions +export function filterInPlace( + arr: T[], + predicate: (value: T, index: number, array: T[]) => boolean +): T[] { + let write = 0; + for (let read = 0; read < arr.length; read++) { + const val = arr[read]; + if (predicate(val, read, arr)) { + arr[write++] = val; + } + } + arr.length = write; + return arr; +} + +export function replaceInPlace( + arr: T[], + predicate: (value: T, index: number, array: T[]) => boolean, + replacement: T +): number { + const idx = arr.findIndex(predicate); + if (idx !== -1) arr[idx] = replacement; + return idx; +} + +export interface FacebookError { + error: { + message: string; + type: string; + code: number; + error_subcode?: number; + fbtrace_id?: string; + } +} + +export enum ResultType { + Ok = "ok", + Err = "error", +} + +export interface Ok { + type: ResultType.Ok; + value: T; +} + +export interface Err { + type: ResultType.Err; + error: E; +} + +export type Result = Ok | Err; From d66cf7321e4aac868101e7d1f97cc0d643381ab7 Mon Sep 17 00:00:00 2001 From: Wolfdragon24 Date: Thu, 10 Jul 2025 11:10:31 +1000 Subject: [PATCH 3/3] Fix to read fetched events data correctly --- backend/src/data/eventData.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/data/eventData.ts b/backend/src/data/eventData.ts index 6030f39..b188863 100644 --- a/backend/src/data/eventData.ts +++ b/backend/src/data/eventData.ts @@ -59,13 +59,13 @@ export async function fetchEvents() { `https://graph.facebook.com/${FB_API_VERSION}/${process.env.FB_EVENT_PAGE_ID}/events?access_token=${process.env.FB_ACCESS_TOKEN}&fields=id,name,cover,place,start_time,end_time` ); - const res: Result = await response.json(); - if (!res || res.type === ResultType.Err) { - console.log(`No events found...\n${res}`); + if (!response || response.type === ResultType.Err) { + console.log(`No events found...\n${response}`); return []; } + const res: FacebookEventsResponse = await response.json(); - const processed = res.value.data.map( + const processed = res.data.map( (e) => new EventInfo( e.id,