summaryrefslogtreecommitdiff
path: root/src/utils/webmentions.ts
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-03 10:56:21 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-03 10:56:21 +0300
commit456cf011b36de91c9936994b1fa45703adcd309b (patch)
tree8e60daf998f731ac50d100fa490eaecae1168042 /src/utils/webmentions.ts
Initial fork of chrismwilliams/astro-theme-cactus theme
Diffstat (limited to 'src/utils/webmentions.ts')
-rw-r--r--src/utils/webmentions.ts115
1 files changed, 115 insertions, 0 deletions
diff --git a/src/utils/webmentions.ts b/src/utils/webmentions.ts
new file mode 100644
index 0000000..c9f62b7
--- /dev/null
+++ b/src/utils/webmentions.ts
@@ -0,0 +1,115 @@
+import * as fs from "node:fs";
+import { WEBMENTION_API_KEY } from "astro:env/server";
+import type { WebmentionsCache, WebmentionsChildren, WebmentionsFeed } from "@/types";
+
+const DOMAIN = import.meta.env.SITE;
+const CACHE_DIR = ".data";
+const filePath = `${CACHE_DIR}/webmentions.json`;
+const validWebmentionTypes = ["like-of", "mention-of", "in-reply-to"];
+
+const hostName = new URL(DOMAIN).hostname;
+
+// Calls webmention.io api.
+async function fetchWebmentions(timeFrom: string | null, perPage = 1000) {
+ if (!DOMAIN) {
+ console.warn("No domain specified. Please set in astro.config.ts");
+ return null;
+ }
+
+ if (!WEBMENTION_API_KEY) {
+ console.warn("No webmention api token specified in .env");
+ return null;
+ }
+
+ let url = `https://webmention.io/api/mentions.jf2?domain=${hostName}&token=${WEBMENTION_API_KEY}&sort-dir=up&per-page=${perPage}`;
+
+ if (timeFrom) url += `&since${timeFrom}`;
+
+ const res = await fetch(url);
+
+ if (res.ok) {
+ const data = (await res.json()) as WebmentionsFeed;
+ return data;
+ }
+
+ return null;
+}
+
+// Merge cached entries [a] with fresh webmentions [b], merge by wm-id
+function mergeWebmentions(a: WebmentionsCache, b: WebmentionsFeed): WebmentionsChildren[] {
+ return Array.from(
+ [...a.children, ...b.children]
+ .reduce((map, obj) => map.set(obj["wm-id"], obj), new Map())
+ .values(),
+ );
+}
+
+// filter out WebmentionChildren
+export function filterWebmentions(webmentions: WebmentionsChildren[]) {
+ return webmentions.filter((webmention) => {
+ // make sure the mention has a property so we can sort them later
+ if (!validWebmentionTypes.includes(webmention["wm-property"])) return false;
+
+ // make sure 'mention-of' or 'in-reply-to' has text content.
+ if (webmention["wm-property"] === "mention-of" || webmention["wm-property"] === "in-reply-to") {
+ return webmention.content && webmention.content.text !== "";
+ }
+
+ return true;
+ });
+}
+
+// save combined webmentions in cache file
+function writeToCache(data: WebmentionsCache) {
+ const fileContent = JSON.stringify(data, null, 2);
+
+ // create cache folder if it doesn't exist already
+ if (!fs.existsSync(CACHE_DIR)) {
+ fs.mkdirSync(CACHE_DIR);
+ }
+
+ // write data to cache json file
+ fs.writeFile(filePath, fileContent, (err) => {
+ if (err) throw err;
+ console.log(`Webmentions saved to ${filePath}`);
+ });
+}
+
+function getFromCache(): WebmentionsCache {
+ if (fs.existsSync(filePath)) {
+ const data = fs.readFileSync(filePath, "utf-8");
+ return JSON.parse(data);
+ }
+ // no cache found
+ return {
+ lastFetched: null,
+ children: [],
+ };
+}
+
+async function getAndCacheWebmentions() {
+ const cache = getFromCache();
+ const mentions = await fetchWebmentions(cache.lastFetched);
+
+ if (mentions) {
+ mentions.children = filterWebmentions(mentions.children);
+ const webmentions: WebmentionsCache = {
+ lastFetched: new Date().toISOString(),
+ // Make sure the first arg is the cache
+ children: mergeWebmentions(cache, mentions),
+ };
+
+ writeToCache(webmentions);
+ return webmentions;
+ }
+
+ return cache;
+}
+
+let webMentions: WebmentionsCache;
+
+export async function getWebmentionsForUrl(url: string) {
+ if (!webMentions) webMentions = await getAndCacheWebmentions();
+
+ return webMentions.children.filter((entry) => entry["wm-target"] === url);
+}