Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat/admin portal #1

Merged
merged 2 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,4 @@ dist
.pnp.*

.firebase
.firebaseEmulators
6 changes: 5 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { defineConfig } from "astro/config";

import react from "@astrojs/react";

// https://astro.build/config
export default defineConfig({});
export default defineConfig({
integrations: [react()],
});
3 changes: 1 addition & 2 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if true;
allow write: if false;
allow read, write: if true;
}
}
}
2 changes: 1 addition & 1 deletion functions/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default {
"firebase-admin/storage",
"firebase-admin/app",
"firebase-admin/firestore",
"express"
"express",
],
output: {
dir: "lib",
Expand Down
10 changes: 5 additions & 5 deletions functions/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { onRequest } from "firebase-functions/v2/https";
const expressApp = express();

routes.forEach(({ route, method, handler }) => {
if (method === "GET") expressApp.get(route, handler);
if (method === "DELETE") expressApp.delete(route, handler);
if (method === "PUT") expressApp.put(route, handler);
if (method === "PATCH") expressApp.patch(route, handler);
if (method === "POST") expressApp.post(route, handler);
if (method === "GET") expressApp.get(route, handler);
if (method === "DELETE") expressApp.delete(route, handler);
if (method === "PUT") expressApp.put(route, handler);
if (method === "PATCH") expressApp.patch(route, handler);
if (method === "POST") expressApp.post(route, handler);
});

// Found at localhost:5001/capes-in-the-dark/us-central1/api/ROUTE
Expand Down
2 changes: 1 addition & 1 deletion functions/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./api";
export * from "./api";
20 changes: 9 additions & 11 deletions functions/src/api/routes/capesInTheWestMarch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ import { Route } from "./types";
import { getMostRecentRss } from "../../utils/firestore";

export const citwmFeed: Route = {
method: "GET",
route: "/capes-in-the-west-march/rss.xml",
handler: async function (_req: Request, res: Response) {
try {
const rssFeed = await getMostRecentRss("Capes in the West March");
res.status(200).send(rssFeed.rss);
}
catch (ex) {
res.sendStatus(404);
}
method: "GET",
route: "/capes-in-the-west-march/rss.xml",
handler: async function (_req: Request, res: Response) {
try {
const rssFeed = await getMostRecentRss("Capes in the West March");
res.status(200).send(rssFeed.rss);
} catch (ex) {
res.sendStatus(404);
}
},
};

5 changes: 1 addition & 4 deletions functions/src/api/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@ import { Route } from "./types";
import { ping } from "./ping";
import { citwmFeed } from "./capesInTheWestMarch";

export const routes: Route[] = [
ping,
citwmFeed,
];
export const routes: Route[] = [ping, citwmFeed];
10 changes: 5 additions & 5 deletions functions/src/api/routes/ping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Request, Response } from "express";
import { Route } from "./types";

export const ping: Route = {
method: "GET",
route: "/ping",
handler: async function (_req: Request, res: Response) {
res.status(200).send("pong");
}
method: "GET",
route: "/ping",
handler: async function (_req: Request, res: Response) {
res.status(200).send("pong");
},
};
8 changes: 4 additions & 4 deletions functions/src/api/routes/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response } from "express";

export type Route = {
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
route: string;
handler: (req: Request, res: Response) => Promise<void>;
}
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
route: string;
handler: (req: Request, res: Response) => Promise<void>;
};
38 changes: 22 additions & 16 deletions functions/src/rss/rebuildRssFeed.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import * as logger from "firebase-functions/logger";

import { getDataFromEvent, queryEpisodeByFeed, queryFeed, saveFeed } from "../utils/firestore";
import { PodcastEpisode } from "../utils/types"
import {
getDataFromEvent,
queryEpisodeByFeed,
queryFeed,
saveFeed,
} from "../utils/firestore";
import { PodcastEpisode } from "../utils/types";
import { makeRss } from "../utils/makeRss";

/**
* Update the RSS Feed Document with a new episode once it's created.
*/
export const rebuildRssFeed = onDocumentCreated("api/v1/episodes/{episodeId}", async (event) => {
export const rebuildRssFeed = onDocumentCreated(
"api/v1/episodes/{episodeId}",
async (event) => {
try {
const episodeData = getDataFromEvent<PodcastEpisode>(event);
if (!episodeData) throw new Error("Unable to retrieve data for new episode");
const feed = await queryFeed(episodeData.feed);
const episodes = await queryEpisodeByFeed(episodeData.feed);
const newRssFeed = makeRss(feed, episodes);
saveFeed(episodeData.feed, newRssFeed);
const episodeData = getDataFromEvent<PodcastEpisode>(event);
if (!episodeData)
throw new Error("Unable to retrieve data for new episode");
const feed = await queryFeed(episodeData.feed);
const episodes = await queryEpisodeByFeed(episodeData.feed);
const newRssFeed = makeRss(feed, episodes);
saveFeed(episodeData.feed, newRssFeed);
} catch (ex) {
logger.error(ex);
logger.error("Aborting...");
}
catch (ex) {
logger.error(ex);
logger.error("Aborting...");
}
});


},
);
26 changes: 15 additions & 11 deletions functions/src/utils/firestore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,29 @@ export async function queryFeed(feed: string) {
throw new Error(`Unable to find feed "${feed}"`);
}


/**
* Saves a new document with the whole RSS feed for a given feed.
* @param data string form of RSS data
*/
export async function saveFeed(feed: string, data: string) {
const db = getFirestoreDb();
return db.collection("api/v1/rss").add({
feed,
rss: data,
timestamp: Timestamp.now(),
});
const db = getFirestoreDb();
return db.collection("api/v1/rss").add({
feed,
rss: data,
timestamp: Timestamp.now(),
});
}

export async function getMostRecentRss(feed: string) {
const db = getFirestoreDb();
const feedItems = await db.collection("api/v1/rss").where("feed", "==", feed).orderBy("timestamp", "desc").limit(1).get();
if (feedItems.size === 1) return feedItems.docs[0].data() as RssDocument;
throw new Error(`Unable to find an RSS feed for "${feed}"`);
const db = getFirestoreDb();
const feedItems = await db
.collection("api/v1/rss")
.where("feed", "==", feed)
.orderBy("timestamp", "desc")
.limit(1)
.get();
if (feedItems.size === 1) return feedItems.docs[0].data() as RssDocument;
throw new Error(`Unable to find an RSS feed for "${feed}"`);
}

/**
Expand Down
30 changes: 16 additions & 14 deletions functions/src/utils/makeRss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Timestamp } from "firebase-admin/firestore";
import { PodcastChannel, PodcastEpisode } from "./types";

export function makeRss(feed: PodcastChannel, episodes: PodcastEpisode[]) {
return `<?xml version="1.0" encoding="UTF-8"?>
return `<?xml version="1.0" encoding="UTF-8"?>
<rss
version="2.0"
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
Expand All @@ -19,7 +19,7 @@ export function makeRss(feed: PodcastChannel, episodes: PodcastEpisode[]) {
}

function makeChannel(channel: PodcastChannel, items: string) {
return `<channel>
return `<channel>
<title>${channel.title}</title>
<description><![CDATA[${channel.description}]]></description>
<link>${channel.contact.site}</link>
Expand All @@ -40,27 +40,29 @@ function makeChannel(channel: PodcastChannel, items: string) {
<podcast:locked>${channel.metadata.locked}</podcast:locked>
<itunes:complete>${channel.metadata.complete}</itunes:complete>
${items}
</channel>`
</channel>`;
}

function formatCategories(categories: PodcastChannel["metadata"]["categories"]) {
return categories
.map(({ category, subCategory }) => {
if (subCategory === undefined)
return `<itunes:category text="${category}"/>`;
return `
function formatCategories(
categories: PodcastChannel["metadata"]["categories"],
) {
return categories
.map(({ category, subCategory }) => {
if (subCategory === undefined)
return `<itunes:category text="${category}"/>`;
return `
<itunes:category text="${category}">
<itunes:category text="${subCategory}"/>
</itunes:category>
`;
})
.join("");
})
.join("");
}

function makeItem(episodeData: PodcastEpisode) {
return `<item>
return `<item>
<title>${episodeData.title}</title>
<link${episodeData.fileData.url}</link>
<link>${episodeData.fileData.url}</link>
<guid isPermaLink="true"><![CDATA[${episodeData.fileData.url}]]></guid>
<description><![CDATA[${episodeData.description}]]></description>
<pubDate>${new Timestamp(episodeData.publishDate.seconds, episodeData.publishDate.nanoseconds).toDate().toUTCString()}</pubDate>
Expand Down Expand Up @@ -114,4 +116,4 @@ const example = `
</channel>
</rss>
`;
*/
*/
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,27 @@
"deploy": "yarn build && firebase deploy",
"lint": "eslint src --ext .astro,.js,.ts,.json",
"format": "prettier --write .",
"emulators": "firebase emulators:start",
"emulators:seed": "tsx scripts/seed.ts"
"emulators": "yarn emulators:start",
"emulators:start": "firebase emulators:start --import .firebaseEmulators",
"emulators:clean": "yarn emulators:clear && firebase emulators:start --export-on-exit --import .firebaseEmulators",
"emulators:save": "firebase emulators:start",
"emulators:seed": "tsx scripts/seed.ts",
"emulators:set": "tsx scripts/set.ts",
"emulators:reset": "yarn emulators:clear && yarn emulators:set",
"emulators:clear": "rm -rf .firebaseEmulators",
"fetch:citwm": "curl https://capes-in-the-dark.web.app/feeds/capes-in-the-west-march/rss.xml",
"fetch:citwm:emulated": "curl localhost:5001/capes-in-the-dark/us-central1/feeds/capes-in-the-west-march/rss.xml"
},
"dependencies": {
"@astrojs/check": "^0.5.6",
"@astrojs/react": "^3.1.0",
"@astrojs/rss": "^4.0.5",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"astro": "^4.4.15",
"firebase": "^10.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"typescript": "5.4"
},
"devDependencies": {
Expand Down
26 changes: 26 additions & 0 deletions scripts/emulators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { spawn } from "child_process";

async function sleep(ms: number): Promise<void> {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

export async function startEmulators(): Promise<() => boolean> {
const emulatorChildProcess = spawn("firebase", [
"emulators:start",
"--only",
"firestore", // "auth,firestore",
"--export-on-exit=./.firebaseEmulators/",
]);
await sleep(10000);
return () => emulatorChildProcess.kill("SIGINT");
}

export async function taskWithEmulatorsOn(
asyncTask: () => Promise<unknown>,
): Promise<void> {
const killEmulatorProc = await startEmulators();
await asyncTask();
killEmulatorProc();
}
Loading
Loading