Skip to content

Commit 93b261c

Browse files
author
Yankai Zhu
committed
Added backend events API
1 parent 5aa59df commit 93b261c

File tree

10 files changed

+309
-12
lines changed

10 files changed

+309
-12
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ yarn-error.log*
3535
# typescript
3636
*.tsbuildinfo
3737
next-env.d.ts
38+
39+
*.env

backend/src/controllers/events.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { RequestHandler } from "express";
2+
import { eventInfo } from "../data/eventData";
3+
4+
export const EventsHandler: RequestHandler = (req, res) => {
5+
res.status(200).json(eventInfo);
6+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import crypto from 'crypto';
2+
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+
}
13+
14+
interface FacebookWebhookNotificationEntry {
15+
id: string;
16+
changes: ChangesEntry[];
17+
}
18+
19+
interface FacebookWebhookNotification {
20+
entry: FacebookWebhookNotificationEntry[];
21+
object: string;
22+
}
23+
24+
const verifySignature = (rawBody: Buffer, signatureHeader?: string): boolean => {
25+
if (!signatureHeader) return false;
26+
const [algo, signature] = signatureHeader.split('=');
27+
if (algo !== 'sha256') return false;
28+
29+
const expected = crypto
30+
.createHmac('sha256', process.env.FB_APP_SECRET as string)
31+
.update(rawBody)
32+
.digest('hex');
33+
34+
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
35+
}
36+
37+
export const EventsWebhookVerifier: RequestHandler = (req, res) => {
38+
const mode = req.query["hub.mode"];
39+
const token = req.query["hub.verify_token"];
40+
const challenge = req.query["hub.challenge"];
41+
42+
if (mode === "subscribe" && token === process.env.FB_WEBHOOK_VERIFY_TOKEN) {
43+
return res.status(200).send(challenge);
44+
}
45+
46+
res.sendStatus(403);
47+
};
48+
49+
/*
50+
Sample webhook payload
51+
https://developers.facebook.com/docs/graph-api/webhooks/getting-started/webhooks-for-pages -- for the outer wrapper
52+
https://developers.facebook.com/docs/graph-api/webhooks/reference/page/#feed -- for the inner objects
53+
54+
{
55+
"object": "page",
56+
"entry": [
57+
{
58+
"id": "PAGE_ID",
59+
"time": 1623242342342,
60+
"changes": [
61+
{
62+
"field": "events",
63+
"value": {
64+
"event_id": "123456789",
65+
"verb": "create", // also "edit" or "delete"
66+
"published": 1
67+
}
68+
}
69+
]
70+
}
71+
]
72+
}
73+
*/
74+
75+
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)) {
78+
return res.sendStatus(401);
79+
}
80+
81+
const notif: FacebookWebhookNotification = req.body;
82+
if (!notif || !notif.entry || notif.object !== "page" || notif.entry.length === 0) {
83+
return res.sendStatus(400);
84+
}
85+
86+
for (const entry of notif.entry) {
87+
if (entry.id !== process.env.FB_EVENT_PAGE_ID) continue;
88+
89+
for (const change of entry.changes) {
90+
if (change.field !== "feed" || change.value.item !== "event") continue;
91+
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 {
97+
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}`);
101+
}
102+
}
103+
}
104+
}
105+
106+
res.sendStatus(200);
107+
}

backend/src/data/eventData.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
class EventInfo {
2+
// god forbid a class have public members
3+
public id: string;
4+
public title: string;
5+
public startTime: string;
6+
public endTime?: string;
7+
public location: string;
8+
public imageUrl: string;
9+
public link: string;
10+
11+
constructor(
12+
id: string,
13+
title: string,
14+
startTime: string,
15+
endTime: string | undefined,
16+
location: string,
17+
imageUrl: string
18+
) {
19+
this.id = id;
20+
this.title = title;
21+
this.startTime = startTime;
22+
this.endTime = endTime;
23+
this.location = location;
24+
this.imageUrl = imageUrl;
25+
// would use link as getter but getters are not enumerable so it doesn't appear in JSON.stringify :skull:
26+
// maybe a cursed fix would be to use Object.defineProperty LOL
27+
this.link = `https://www.facebook.com/events/${id}`;
28+
}
29+
}
30+
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+
64+
interface FacebookEvent {
65+
id: string;
66+
name: string;
67+
cover?: { source: string };
68+
place?: { name: string };
69+
start_time: string;
70+
end_time?: string;
71+
}
72+
73+
interface FacebookEventsResponse {
74+
data: FacebookEvent[];
75+
}
76+
77+
// this isn't in .env for different module compatiblity
78+
const FB_API_VERSION = "v23.0";
79+
80+
export async function fetchEvents() {
81+
const response = await fetch(
82+
`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`
83+
);
84+
85+
const res: FacebookEventsResponse = await response.json();
86+
87+
if (!res || !res.data) {
88+
console.log("No events found...");
89+
return;
90+
}
91+
92+
const processed = res.data.map(
93+
(e) =>
94+
new EventInfo(
95+
e.id,
96+
e.name,
97+
e.start_time,
98+
e.end_time,
99+
e.place?.name ?? "Everything everywhere all at once!!!",
100+
e.cover?.source || "/images/events/default_event.jpg"
101+
)
102+
);
103+
104+
eventInfo = processed;
105+
}
106+
107+
export async function fetchEvent(id: string) {
108+
const response = await fetch(
109+
`https://graph.facebook.com/${FB_API_VERSION}/${id}?access_token=${process.env.FB_ACCESS_TOKEN}&fields=id,name,cover,place,start_time,end_time`
110+
);
111+
112+
const res: FacebookEvent = await response.json();
113+
114+
if (!res) {
115+
throw new Error(`Couldn't get details for event ${id}`);
116+
}
117+
118+
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"
125+
);
126+
}

backend/src/index.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,39 @@ import express, { Express } from "express";
22
import cors from "cors";
33
import dotenv from "dotenv";
44
import pingRoute from "./routes/ping";
5+
import eventsRoute from "./routes/events";
6+
import eventsWebhookRoute from "./routes/eventsWebhook";
7+
import { fetchEvents } from "./data/eventData";
58

69
dotenv.config();
710

8-
const app: Express = express();
9-
const port = process.env.PORT || 9000;
11+
(async () => {
12+
try {
13+
await fetchEvents();
14+
console.log("Events fetched successfully");
15+
} catch (error) {
16+
// do we ungracefully bail out here???
17+
console.error("Error fetching events:", error);
18+
}
1019

11-
// Middleware
12-
app.use(express.json());
13-
app.use(cors());
14-
15-
app.use(pingRoute);
16-
17-
app.listen(port, () => {
18-
console.log(`Server successfully started on port ${port}`);
19-
});
20+
const app: Express = express();
21+
const port = process.env.PORT || 9000;
22+
23+
// Middleware
24+
app.use(
25+
express.json({
26+
verify: (req, res, buf) => {
27+
req.rawBody = buf;
28+
},
29+
})
30+
);
31+
app.use(cors());
32+
33+
app.use(pingRoute);
34+
app.use(eventsWebhookRoute);
35+
app.use(eventsRoute);
36+
37+
app.listen(port, () => {
38+
console.log(`Server successfully started on port ${port}`);
39+
});
40+
})();

backend/src/routes/events.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Router } from "express";
2+
import { EventsHandler } from "../controllers/events";
3+
4+
const router = Router();
5+
6+
router.get("/events", EventsHandler);
7+
8+
export default router;

backend/src/routes/eventsWebhook.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Router } from "express";
2+
import { EventsWebhookUpdate, EventsWebhookVerifier } from "../controllers/eventsWebhook";
3+
4+
const router = Router();
5+
6+
router.post("/eventsWebhook", EventsWebhookUpdate);
7+
router.get("/eventsWebhook", EventsWebhookVerifier);
8+
9+
export default router;

backend/src/types/express/index.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import "express";
2+
3+
declare module "express-serve-static-core" {
4+
interface Request {
5+
rawBody?: Buffer;
6+
}
7+
8+
interface IncomingMessage {
9+
rawBody?: Buffer;
10+
}
11+
}

backend/src/types/http/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import "http";
2+
3+
declare module "http" {
4+
interface IncomingMessage {
5+
rawBody?: Buffer;
6+
}
7+
}

backend/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
3232
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
3333
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
34-
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
34+
"typeRoots": ["./src/types"], /* Specify multiple folders that act like './node_modules/@types'. */
3535
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
3636
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
3737
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */

0 commit comments

Comments
 (0)