From 5593878c66b6d5af7615dda7ddd9ee6ba3adf3c9 Mon Sep 17 00:00:00 2001 From: Julien Oculi Date: Tue, 2 Jul 2024 11:20:38 +0200 Subject: [PATCH] feat(backend): :sparkles: implement news fetching from git.cohabit --- .vscode/settings.json | 4 +- src/blog/mod.ts | 104 ++++++++++++++++++++++++++++++++++++++++++ src/blog/types.ts | 12 +++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/blog/mod.ts create mode 100644 src/blog/types.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 8f31af9..7d3d80b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,7 +37,9 @@ "ux", "route", "frontend", - "components" + "components", + "island", + "backend" ], "[ignore]": { "editor.defaultFormatter": "foxundermoon.shell-format" diff --git a/src/blog/mod.ts b/src/blog/mod.ts new file mode 100644 index 0000000..779105e --- /dev/null +++ b/src/blog/mod.ts @@ -0,0 +1,104 @@ +import { BlogProps } from ':components/BlogCard.tsx' +import { NewsFrontMatter } from ':src/blog/types.ts' +import { base64ToString } from ':src/utils.ts' +import { extract } from '@std/front-matter/yaml' + +export async function fetchNews( + publisher: string, + name: string, +): Promise { + const apiUrl = 'https://git.cohabit.fr/api/v1/' + const baseEndpoint = new URL(`repos/${publisher}/.news/`, apiUrl) + const endpoint = new URL('contents/', baseEndpoint) + + // Get readme content api url + const readmePath = encodeURIComponent(`${name}/README.md`) + const contentUrl = new URL(readmePath, endpoint) + + // Fetch readme content, commit hash and raw url for relative links + const file = await getCommitAndContent(contentUrl) + // Get commit infos (author + date) and get readme content from base64 source + const { raw, url, lastUpdate, author } = await getAuthorAndParseContent( + file, + baseEndpoint, + ) + // Extract frontmatter + const { attrs, body } = extract(raw) + + // Transform API responses into BlogProps for BlogCard and BlogPost components + return { + author, + publisher, + lastUpdate, + options: attrs['x-cohabit'], + title: attrs.title, + hash: file.sha, + description: attrs.description, + body, + name, + url, + tags: attrs.tags, + } +} + +export async function* fetchNewsList( + publisher: string, +): AsyncGenerator { + const apiUrl = 'https://git.cohabit.fr/api/v1/' + const baseEndpoint = new URL(`repos/${publisher}/.news/`, apiUrl) + const endpoint = new URL('contents/', baseEndpoint) + + // Fetch repo content + const root = await fetch(endpoint).then((response) => response.json()) as { + name: string + type: string + }[] + + // Fetch `README.md` in sub directories + const blogPropsList = root + // Remove file and dir starting with "." + .filter(isNewsDirectory) + // Fetch single news and return BlogProps + .map(({ name }) => fetchNews(publisher, name)) + + // Yield each news + for (const blogProps of blogPropsList) { + yield blogProps + } +} + +async function getAuthorAndParseContent( + file: { download_url: string; content: string; last_commit_sha: string }, + baseEndpoint: URL, +) { + const commitUrl = new URL( + `git/commits/${file.last_commit_sha}?stat=false&verification=false&files=false`, + baseEndpoint, + ) + const infos = await fetch(commitUrl).then((response) => + response.json() + ) as { + created: string + author: { login: string } + } + + return { + raw: base64ToString(file.content), + url: file.download_url, + lastUpdate: new Date(infos.created), + author: infos.author.login, + } +} + +async function getCommitAndContent(contentUrl: URL) { + return await fetch(contentUrl).then((response) => response.json()) as { + download_url: string + content: string + sha: string + last_commit_sha: string + } +} + +function isNewsDirectory(entry: { name: string; type: string }): boolean { + return entry.type === 'dir' && entry.name.startsWith('.') === false +} diff --git a/src/blog/types.ts b/src/blog/types.ts new file mode 100644 index 0000000..d035bea --- /dev/null +++ b/src/blog/types.ts @@ -0,0 +1,12 @@ +export type NewsFrontMatter = { + title: string + description: string + tags?: string[] + 'x-cohabit': { + links?: Record[] + status: 'canceled' | 'futur' | 'current' | 'finished' + visibility?: 'public' | 'internal' + thumbnail: string + dueDate: string + } +}