-
-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
289d01a
commit c727c02
Showing
10 changed files
with
983 additions
and
5,457 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. | |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
92 changes: 92 additions & 0 deletions
92
packages/endpoint-webmention-io/lib/controllers/webmentions.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
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 = { | ||
name: getAuthorName(item), | ||
url: item.author.url, | ||
avatar: { | ||
src: item.author.photo, | ||
}, | ||
}; | ||
|
||
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); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{% extends "document.njk" %} | ||
|
||
{% block content %} | ||
{%- if webmentions.length > 0 %} | ||
{% for item in webmentions %} | ||
{{ user({ | ||
avatar: { | ||
src: item.user.avatar.src | ||
}, | ||
name: item.user.name, | ||
meta: __("webmention-io.mention." + item["wm-property"], item["wm-target"]) | urlize, | ||
url: item.user.url | ||
}) | indent(6) }} | ||
{{ card({ | ||
title: item.title, | ||
icon: item.icon, | ||
description: item.description if item.description.html, | ||
permalink: item.url, | ||
published: item.published | ||
}) | indent(6) }} | ||
<hr> | ||
{% endfor %} | ||
{{ pagination(cursor) }} | ||
{%- else -%} | ||
{{ prose({ text: __("webmention-io.webmentions.none") }) }} | ||
{%- endif %} | ||
{% endblock %} |