Skip to content

Commit 36b5791

Browse files
author
Yankai Zhu
committed
polished code
1 parent 93b261c commit 36b5791

File tree

6 files changed

+173
-94
lines changed

6 files changed

+173
-94
lines changed

backend/package-lock.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"@types/cors": "^2.8.17",
1010
"@types/express": "^4.17.21",
1111
"@types/node": "^20.12.13",
12+
"async-mutex": "^0.5.0",
1213
"cors": "^2.8.5",
1314
"dotenv": "^16.4.5",
1415
"express": "^4.19.2",
Lines changed: 67 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,38 @@
1-
import crypto from 'crypto';
1+
import crypto from "crypto";
22
import { RequestHandler } from "express";
3-
import { eventInfo, fetchEvent, filterInPlace, replaceInPlace } from '../data/eventData';
4-
5-
interface ChangesEntry {
6-
field: string;
7-
value: {
8-
event_id: string;
9-
item: string;
10-
verb: string;
11-
}
12-
}
3+
import { eventInfo, eventInfoMutex, fetchEvent } from "../data/eventData";
4+
import { filterInPlace, replaceInPlace } from "../util";
135

14-
interface FacebookWebhookNotificationEntry {
15-
id: string;
16-
changes: ChangesEntry[];
17-
}
18-
19-
interface FacebookWebhookNotification {
20-
entry: FacebookWebhookNotificationEntry[];
6+
interface FacebookWebhookPayload {
217
object: string;
8+
entry: Array<{
9+
id: string;
10+
changes: Array<{
11+
field: string;
12+
value: {
13+
event_id: string;
14+
item: string;
15+
verb: string;
16+
};
17+
}>;
18+
}>;
2219
}
2320

24-
const verifySignature = (rawBody: Buffer, signatureHeader?: string): boolean => {
21+
const verifySignature = (
22+
rawBody: Buffer,
23+
signatureHeader?: string
24+
): boolean => {
2525
if (!signatureHeader) return false;
26-
const [algo, signature] = signatureHeader.split('=');
27-
if (algo !== 'sha256') return false;
26+
const [algo, signature] = signatureHeader.split("=");
27+
if (algo !== "sha256") return false;
2828

2929
const expected = crypto
30-
.createHmac('sha256', process.env.FB_APP_SECRET as string)
30+
.createHmac("sha256", process.env.FB_APP_SECRET as string)
3131
.update(rawBody)
32-
.digest('hex');
32+
.digest("hex");
3333

3434
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
35-
}
35+
};
3636

3737
export const EventsWebhookVerifier: RequestHandler = (req, res) => {
3838
const mode = req.query["hub.mode"];
@@ -73,13 +73,22 @@ https://developers.facebook.com/docs/graph-api/webhooks/reference/page/#feed --
7373
*/
7474

7575
export const EventsWebhookUpdate: RequestHandler = async (req, res) => {
76-
const signature = req.headers['x-hub-signature-256'];
77-
if (!req.rawBody || typeof signature !== "string" || !verifySignature(req.rawBody, signature)) {
76+
const signature = req.headers["x-hub-signature-256"];
77+
if (
78+
!req.rawBody ||
79+
typeof signature !== "string" ||
80+
!verifySignature(req.rawBody, signature)
81+
) {
7882
return res.sendStatus(401);
7983
}
8084

81-
const notif: FacebookWebhookNotification = req.body;
82-
if (!notif || !notif.entry || notif.object !== "page" || notif.entry.length === 0) {
85+
const notif: FacebookWebhookPayload = req.body;
86+
if (
87+
!notif ||
88+
!notif.entry ||
89+
notif.object !== "page" ||
90+
notif.entry.length === 0
91+
) {
8392
return res.sendStatus(400);
8493
}
8594

@@ -89,19 +98,40 @@ export const EventsWebhookUpdate: RequestHandler = async (req, res) => {
8998
for (const change of entry.changes) {
9099
if (change.field !== "feed" || change.value.item !== "event") continue;
91100

92-
if (change.value.verb === "delete") {
93-
// we need filter *in place* because all imports are immutable (the REAL const)
94-
filterInPlace(eventInfo, (val, index, arr) => val.id !== change.value.event_id);
95-
} else {
96-
try {
101+
try {
102+
if (change.value.verb === "delete") {
103+
await eventInfoMutex.runExclusive(() =>
104+
filterInPlace(eventInfo, (val) => val.id !== change.value.event_id)
105+
);
106+
console.log(`Deleted event: ${change.value.event_id}`);
107+
} else if (change.value.verb === "edit") {
108+
const newEvent = await fetchEvent(change.value.event_id);
109+
110+
eventInfoMutex.runExclusive(() =>
111+
replaceInPlace(
112+
eventInfo,
113+
(val) => val.id === change.value.event_id,
114+
newEvent
115+
)
116+
);
117+
console.log(`Edited event: ${change.value.event_id}`);
118+
} else if (change.value.verb === "add") {
97119
const newEvent = await fetchEvent(change.value.event_id);
98-
replaceInPlace(eventInfo, (val, index, arr) => val.id === change.value.event_id, newEvent);
99-
} catch(err) {
100-
console.log(`Wasn't able to update event for some reason: ${err}`);
120+
await eventInfoMutex.runExclusive(() => eventInfo.push(newEvent));
121+
console.log(`Added event: ${change.value.event_id}`);
122+
} else {
123+
console.warn(
124+
`Unknown verb "${change.value.verb}" for event ${change.value.event_id}`
125+
);
101126
}
127+
} catch (err) {
128+
console.error(
129+
`Error processing event: ${change.value.event_id}:\n${err}`
130+
);
131+
return res.sendStatus(500);
102132
}
103133
}
104134
}
105135

106136
res.sendStatus(200);
107-
}
137+
};

backend/src/data/eventData.ts

Lines changed: 31 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { Mutex } from "async-mutex";
2+
import { inspect } from "util";
3+
import { FacebookError, Result, ResultType } from "../util";
4+
15
class EventInfo {
26
// god forbid a class have public members
37
public id: string;
@@ -28,39 +32,6 @@ class EventInfo {
2832
}
2933
}
3034

31-
// We are altering the array in place, pray we do not alter it from another thread
32-
// I don't even know if concurrent modification exception is a thing in JS
33-
// Maybe this is a single threaded moment :icant:
34-
export function filterInPlace<T>(
35-
arr: T[],
36-
predicate: (value: T, index: number, array: T[]) => boolean
37-
): T[] {
38-
let write = 0;
39-
for (let read = 0; read < arr.length; read++) {
40-
const val = arr[read];
41-
if (predicate(val, read, arr)) {
42-
arr[write++] = val;
43-
}
44-
}
45-
arr.length = write;
46-
return arr;
47-
}
48-
49-
// This one is definitely not thread safe lmao
50-
// TODO fix with a mutex probably
51-
export function replaceInPlace<T>(
52-
arr: T[],
53-
predicate: (value: T, index: number, array: T[]) => boolean,
54-
replacement: T
55-
): number {
56-
const idx = arr.findIndex(predicate);
57-
if (idx !== -1) arr[idx] = replacement;
58-
return idx;
59-
}
60-
61-
// we LOVE global variables
62-
export let eventInfo: EventInfo[] = [];
63-
6435
interface FacebookEvent {
6536
id: string;
6637
name: string;
@@ -76,51 +47,60 @@ interface FacebookEventsResponse {
7647

7748
// this isn't in .env for different module compatiblity
7849
const FB_API_VERSION = "v23.0";
50+
const DEFAULT_EVENT_LOCATION = "Everything everywhere all at once!!!";
51+
const DEFAULT_EVENT_IMAGE = "/images/events/default_event.jpg";
52+
53+
// we LOVE global variables
54+
export const eventInfoMutex = new Mutex();
55+
export const eventInfo: EventInfo[] = [];
7956

8057
export async function fetchEvents() {
8158
const response = await fetch(
8259
`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`
8360
);
8461

85-
const res: FacebookEventsResponse = await response.json();
86-
87-
if (!res || !res.data) {
88-
console.log("No events found...");
89-
return;
62+
const res: Result<FacebookEventsResponse, FacebookError> = await response.json();
63+
if (!res || res.type === ResultType.Err) {
64+
console.log(`No events found...\n${res}`);
65+
return [];
9066
}
9167

92-
const processed = res.data.map(
68+
const processed = res.value.data.map(
9369
(e) =>
9470
new EventInfo(
9571
e.id,
9672
e.name,
9773
e.start_time,
9874
e.end_time,
99-
e.place?.name ?? "Everything everywhere all at once!!!",
100-
e.cover?.source || "/images/events/default_event.jpg"
75+
e.place?.name ?? DEFAULT_EVENT_LOCATION,
76+
e.cover?.source ?? DEFAULT_EVENT_IMAGE
10177
)
10278
);
10379

104-
eventInfo = processed;
80+
return processed;
10581
}
10682

10783
export async function fetchEvent(id: string) {
10884
const response = await fetch(
10985
`https://graph.facebook.com/${FB_API_VERSION}/${id}?access_token=${process.env.FB_ACCESS_TOKEN}&fields=id,name,cover,place,start_time,end_time`
11086
);
11187

112-
const res: FacebookEvent = await response.json();
88+
const res: Result<FacebookEvent, FacebookError> = await response.json();
11389

114-
if (!res) {
115-
throw new Error(`Couldn't get details for event ${id}`);
90+
if (!res || res.type === ResultType.Err) {
91+
throw new Error(
92+
`Couldn't fetch details for event ${id}\n${inspect(
93+
Object.getOwnPropertyDescriptor(res, "error")?.value
94+
)}`
95+
);
11696
}
11797

11898
return new EventInfo(
119-
res.id,
120-
res.name,
121-
res.start_time,
122-
res.end_time,
123-
res.place?.name ?? "Everything everywhere all at once!!!",
124-
res.cover?.source || "/images/events/default_event.jpg"
99+
res.value.id,
100+
res.value.name,
101+
res.value.start_time,
102+
res.value.end_time,
103+
res.value.place?.name ?? DEFAULT_EVENT_LOCATION,
104+
res.value.cover?.source ?? DEFAULT_EVENT_IMAGE
125105
);
126106
}

backend/src/index.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@ import dotenv from "dotenv";
44
import pingRoute from "./routes/ping";
55
import eventsRoute from "./routes/events";
66
import eventsWebhookRoute from "./routes/eventsWebhook";
7-
import { fetchEvents } from "./data/eventData";
7+
import { eventInfo, eventInfoMutex, fetchEvents } from "./data/eventData";
88

99
dotenv.config();
1010

1111
(async () => {
1212
try {
13-
await fetchEvents();
13+
const events = await fetchEvents();
14+
eventInfoMutex.runExclusive(() => eventInfo.concat(events));
1415
console.log("Events fetched successfully");
1516
} catch (error) {
1617
// do we ungracefully bail out here???
17-
console.error("Error fetching events:", error);
18+
// could just load from a backup file instead
19+
console.error("Error fetching events on startup:", error);
1820
}
1921

2022
const app: Express = express();
2123
const port = process.env.PORT || 9000;
22-
24+
2325
// Middleware
2426
app.use(
2527
express.json({
@@ -29,11 +31,11 @@ dotenv.config();
2931
})
3032
);
3133
app.use(cors());
32-
34+
3335
app.use(pingRoute);
3436
app.use(eventsWebhookRoute);
3537
app.use(eventsRoute);
36-
38+
3739
app.listen(port, () => {
3840
console.log(`Server successfully started on port ${port}`);
3941
});

0 commit comments

Comments
 (0)