Skip to content

Commit

Permalink
feat(endpoint-webmention-io): webmention.io endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
paulrobertlloyd committed Dec 23, 2024
1 parent 13e0bf8 commit 37f0144
Show file tree
Hide file tree
Showing 10 changed files with 979 additions and 5,457 deletions.
1 change: 1 addition & 0 deletions indiekit.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const config = {
plugins: [
"@indiekit-test/frontend",
"@indiekit/endpoint-json-feed",
"@indiekit/endpoint-webmention-io",
"@indiekit/post-type-audio",
"@indiekit/post-type-event",
"@indiekit/post-type-jam",
Expand Down
6,090 changes: 633 additions & 5,457 deletions package-lock.json

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions packages/endpoint-webmention-io/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# @indiekit/endpoint-webmention-io

Webmention.io endpoint for Indiekit. View webmentions collected by the Webmention.io service.

## Installation

`npm i @indiekit/endpoint-webmention-io`

## Usage

Add `@indiekit/endpoint-webmention-io` to your list of plug-ins, specifying options as required:

```jsonc
{
"plugins": ["@indiekit/endpoint-webmention-io"],
"@indiekit/endpoint-webmention-io": {
"mountPath": "/webmentions",
},
}
```

## Options

| Option | Type | Description |
| :---------- | :------- | :-------------------------------------------------------------------------------------------------------------------- |
| `mountPath` | `string` | Path to management interface. _Optional_, defaults to `/webmentions`. |
| `token` | `string` | [Webmention.io](https://webmention.io/settings) API token. _Required_, defaults to `process.env.WEBMENTION_IO_TOKEN`. |
4 changes: 4 additions & 0 deletions packages/endpoint-webmention-io/assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions packages/endpoint-webmention-io/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import express from "express";

import { webmentionsController } from "./lib/controllers/webmentions.js";

const defaults = {
mountPath: "/webmentions",
token: process.env.WEBMENTION_IO_TOKEN,
};
const router = express.Router();

export default class WebmentionEndpoint {
constructor(options = {}) {
this.name = "Webmention.io endpoint";
this.options = { ...defaults, ...options };
this.mountPath = this.options.mountPath;
}

get navigationItems() {
return {
href: this.options.mountPath,
text: "webmention-io.title",
};
}

get routes() {
router.get("/", webmentionsController);

return router;
}

init(Indiekit) {
Indiekit.addEndpoint(this);
}
}
90 changes: 90 additions & 0 deletions packages/endpoint-webmention-io/lib/controllers/webmentions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { IndiekitError } from "@indiekit/error";

import {
getAuthorName,
getMentionTitle,
getMentionType,
sanitiseHtml,
} from "../utils.js";

/**
* List webmentions
* @type {import("express").RequestHandler}
*/
export const webmentionsController = async (request, response, next) => {
try {
const { application, publication } = request.app.locals;
const domain = new URL(publication.me).hostname;
const limit = Number(request.query.limit) || 20;
const page = Number(request.query.page) || 0;

const endpointUrl = new URL("https://webmention.io");
// TODO: Pass token in top-level function param
endpointUrl.searchParams.append("token", process.env.WEBMENTION_IO_TOKEN);
endpointUrl.searchParams.append("domain", domain);
endpointUrl.searchParams.append("per-page", String(limit));
endpointUrl.pathname = "api/mentions.jf2";

if (page) {
endpointUrl.searchParams.append("page", String(page));
}

const endpointResponse = await fetch(endpointUrl.href, {
headers: {
accept: "application/json",
},
});

if (!endpointResponse.ok) {
throw await IndiekitError.fromFetch(endpointResponse);
}

const body = await endpointResponse.json();

let webmentions;
if (body?.children?.length > 0) {
webmentions = body.children.map((item) => {
let html;
if (item.content?.html) {
html = sanitiseHtml(item.content.html);
} else if (item.content?.text) {
html = `<p>${item.content.text}</p>`;
}

item.id = item["wm-id"];
item.icon = getMentionType(item["wm-property"]);
item.locale = application.locale;
item.title = getMentionTitle(item);
item.description = { html };
item.published = item.published || item["wm-received"];
item.user = {
avatar: { src: item.author.photo },
name: getAuthorName(item),
url: item.author.url,
};

return item;
});
}

const cursor = {};

cursor.next = {
href: `?page=${page + 1}`,
};

if (Number(page) > 0) {
cursor.previous = {
href: `?page=${page - 1}`,
};
}

response.render("webmentions", {
title: response.locals.__("webmention-io.title"),
webmentions,
cursor,
});
} catch (error) {
next(error);
}
};
104 changes: 104 additions & 0 deletions packages/endpoint-webmention-io/lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import sanitize from "sanitize-html";

/**
* Get icon name for Webmention.io `wm-property`
* @param {string} wmProperty - Webmention.io `wm-property` value
* @returns {string} Icon name
*/
export const getMentionType = (wmProperty) => {
switch (true) {
case wmProperty === "in-reply-to": {
return "reply";
}
case wmProperty === "like-of": {
return "like";
}
case wmProperty === "repost-of": {
return "repost";
}
case wmProperty === "bookmark-of": {
return "bookmark";
}
case wmProperty === "rsvp": {
return "rsvp";
}
default: {
return "mention";
}
}
};

const upperFirst = (string) => {
return String(string).charAt(0).toUpperCase() + String(string).slice(1);
};

export const getMentionTitle = (item) => {
let type = getMentionType(item["wm-property"]);
type = upperFirst(type).replace("Rsvp", "RSVP");

return item.name || type;
};

export const getAuthorName = (item) => {
let url = item.author.url || item.url;
if (item.author?.url) {
url = new URL(item.author.url);
url = `${url.hostname}${url.pathname}`;
}

return item.author.name || url;
};

/**
* Normalise paragraphs
* @example `One<br><br>Two` => <p>One</p><p>Two</p>
* @param {string} html - HTML
* @returns {string} HTML with normalised paragraphs
*/
export const normaliseParagraphs = (html) => {
html = `<p>${html}</p>`;
html = html.replaceAll(/<br\s*\/?>\s*<br\s*\/?>/g, "</p><p>");
return html;
};

/**
* Remove line breaks
* @example `One\n\nTwo` => One Two
* @param {string} html - HTML
* @returns {string} HTML with normalised paragraphs
*/
export const removeLineBreaks = (html) => {
return html.replaceAll(/(\r\n|\n|\r)/gm, "");
};

/**
* Sanitise incoming mention HTML
* @param {string} html - HTML
* @returns {string} Sanitised HTML
*/
export const sanitiseHtml = (html) => {
html = removeLineBreaks(html);
html = normaliseParagraphs(html);
html = sanitize(html, {
exclusiveFilter: function (frame) {
// Remove empty Brid.gy links, for example
// <p><a href="https://brid.gy/publish/mastodon"></a></p>
return (
(frame.tag === "a" &&
frame.attribs?.href?.includes("brid.gy") &&
!frame.text.trim()) ||
(frame.tag === "p" && !frame.text.trim())
);
},
transformTags: {
h1: "h3",
h2: "h4",
h3: "h5",
h4: "h6",
h5: "h6",
h6: "h6",
},
});

return html;
};
16 changes: 16 additions & 0 deletions packages/endpoint-webmention-io/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"webmention-io": {
"title": "Webmentions",
"webmentions": {
"none": "No webmentions"
},
"mention": {
"bookmark-of": "bookmarked %s",
"in-reply-to": "replied to %s",
"like-of": "liked %s",
"mention-of": "mentioned %s",
"repost-of": "reposted %s",
"rsvp": "RSVP’d to %s"
}
}
}
45 changes: 45 additions & 0 deletions packages/endpoint-webmention-io/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@indiekit/endpoint-webmention-io",
"version": "1.0.0-beta.19",
"description": "Webmention.io endpoint for Indiekit. View webmentions collected by the Webmention.io service.",
"keywords": [
"indiekit",
"indiekit-plugin",
"indieweb",
"webmention"
],
"homepage": "https://getindiekit.com",
"author": {
"name": "Paul Robert Lloyd",
"url": "https://paulrobertlloyd.com"
},
"license": "MIT",
"engines": {
"node": ">=20"
},
"type": "module",
"main": "index.js",
"files": [
"assets",
"lib",
"locales",
"views",
"index.js"
],
"bugs": {
"url": "https://github.com/getindiekit/indiekit/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/getindiekit/indiekit.git",
"directory": "packages/endpoint-webmention-io"
},
"dependencies": {
"@indiekit/error": "^1.0.0-beta.15",
"express": "^4.17.1",
"sanitize-html": "^2.14.0"
},
"publishConfig": {
"access": "public"
}
}
25 changes: 25 additions & 0 deletions packages/endpoint-webmention-io/views/webmentions.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% extends "document.njk" %}

{% block content %}
{%- if webmentions.length > 0 %}
{% for item in webmentions %}
{{ mention({
user: {
avatar: item.user.avatar,
meta: __("webmention-io.mention." + item["wm-property"], item["wm-target"]) | urlize,
name: item.user.name
},
mention: {
title: item.title,
icon: item.icon,
description: item.description if item.description.html,
permalink: item.url,
published: item.published
}
}) | indent(6) }}
{% endfor %}
{{ pagination(cursor) }}
{%- else -%}
{{ prose({ text: __("webmention-io.webmentions.none") }) }}
{%- endif %}
{% endblock %}

0 comments on commit 37f0144

Please sign in to comment.