@rmdes/indiekit-endpoint-activitypub 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/icon.svg +12 -0
- package/index.js +376 -0
- package/lib/actor.js +75 -0
- package/lib/controllers/activities.js +56 -0
- package/lib/controllers/dashboard.js +39 -0
- package/lib/controllers/followers.js +58 -0
- package/lib/controllers/following.js +58 -0
- package/lib/controllers/migrate.js +121 -0
- package/lib/federation.js +410 -0
- package/lib/inbox.js +291 -0
- package/lib/jf2-to-as2.js +191 -0
- package/lib/keys.js +39 -0
- package/lib/migration.js +184 -0
- package/lib/webfinger.js +43 -0
- package/locales/en.json +41 -0
- package/package.json +51 -0
- package/views/activities.njk +29 -0
- package/views/dashboard.njk +45 -0
- package/views/followers.njk +26 -0
- package/views/following.njk +27 -0
- package/views/migrate.njk +67 -0
package/lib/webfinger.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle WebFinger resource resolution.
|
|
3
|
+
*
|
|
4
|
+
* WebFinger is the discovery mechanism for ActivityPub — when someone
|
|
5
|
+
* searches for @rick@rmendes.net, their server queries:
|
|
6
|
+
* GET /.well-known/webfinger?resource=acct:rick@rmendes.net
|
|
7
|
+
*
|
|
8
|
+
* We return a JRD (JSON Resource Descriptor) pointing to the actor URL
|
|
9
|
+
* so the remote server can then fetch the full actor document.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} resource - The resource query (e.g. "acct:rick@rmendes.net")
|
|
12
|
+
* @param {object} options
|
|
13
|
+
* @param {string} options.handle - Actor handle (e.g. "rick")
|
|
14
|
+
* @param {string} options.hostname - Publication hostname (e.g. "rmendes.net")
|
|
15
|
+
* @param {string} options.actorUrl - Full actor URL (e.g. "https://rmendes.net/")
|
|
16
|
+
* @returns {object|null} JRD response object, or null if resource doesn't match
|
|
17
|
+
*/
|
|
18
|
+
export function handleWebFinger(resource, options) {
|
|
19
|
+
const { handle, hostname, actorUrl } = options;
|
|
20
|
+
const expectedAcct = `acct:${handle}@${hostname}`;
|
|
21
|
+
|
|
22
|
+
// Match both "acct:rick@rmendes.net" and the actor URL itself
|
|
23
|
+
if (resource !== expectedAcct && resource !== actorUrl) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
subject: expectedAcct,
|
|
29
|
+
aliases: [actorUrl],
|
|
30
|
+
links: [
|
|
31
|
+
{
|
|
32
|
+
rel: "self",
|
|
33
|
+
type: "application/activity+json",
|
|
34
|
+
href: actorUrl,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
rel: "http://webfinger.net/rel/profile-page",
|
|
38
|
+
type: "text/html",
|
|
39
|
+
href: actorUrl,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
package/locales/en.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"activitypub": {
|
|
3
|
+
"title": "ActivityPub",
|
|
4
|
+
"followers": "Followers",
|
|
5
|
+
"following": "Following",
|
|
6
|
+
"activities": "Activity log",
|
|
7
|
+
"migrate": "Mastodon migration",
|
|
8
|
+
"recentActivity": "Recent activity",
|
|
9
|
+
"noActivity": "No activity yet. Once your actor is federated, interactions will appear here.",
|
|
10
|
+
"noFollowers": "No followers yet.",
|
|
11
|
+
"noFollowing": "Not following anyone yet.",
|
|
12
|
+
"followerCount": "%d follower",
|
|
13
|
+
"followerCount_plural": "%d followers",
|
|
14
|
+
"followingCount": "%d following",
|
|
15
|
+
"followedAt": "Followed",
|
|
16
|
+
"source": "Source",
|
|
17
|
+
"sourceImport": "Mastodon import",
|
|
18
|
+
"sourceManual": "Manual",
|
|
19
|
+
"sourceFederation": "Federation",
|
|
20
|
+
"direction": "Direction",
|
|
21
|
+
"directionInbound": "Received",
|
|
22
|
+
"directionOutbound": "Sent",
|
|
23
|
+
"migrate.aliasLabel": "Your old Mastodon account URL",
|
|
24
|
+
"migrate.aliasHint": "e.g. https://mstdn.social/users/rmdes — sets alsoKnownAs on your actor",
|
|
25
|
+
"migrate.aliasSave": "Save alias",
|
|
26
|
+
"migrate.importLabel": "Import followers and following",
|
|
27
|
+
"migrate.fileLabel": "Mastodon export CSV",
|
|
28
|
+
"migrate.fileHint": "Upload following_accounts.csv from your Mastodon data export",
|
|
29
|
+
"migrate.importButton": "Import",
|
|
30
|
+
"migrate.importFollowing": "Import following list",
|
|
31
|
+
"migrate.importFollowers": "Import followers list (pending until they re-follow after Move)",
|
|
32
|
+
"migrate.step1Title": "Step 1 — Configure actor alias",
|
|
33
|
+
"migrate.step1Desc": "Link your old Mastodon account to this actor so the fediverse knows they are the same person.",
|
|
34
|
+
"migrate.step2Title": "Step 2 — Import followers/following",
|
|
35
|
+
"migrate.step2Desc": "Upload your Mastodon data export CSV to import your social graph.",
|
|
36
|
+
"migrate.step3Title": "Step 3 — Trigger Move on Mastodon",
|
|
37
|
+
"migrate.step3Desc": "Go to your Mastodon instance → Preferences → Account → Move to a different account. Enter your new handle and confirm. After the Move, followers will automatically re-follow you here.",
|
|
38
|
+
"migrate.success": "Imported %d following, %d followers (%d failed).",
|
|
39
|
+
"migrate.aliasSuccess": "Actor alias updated."
|
|
40
|
+
}
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"indiekit",
|
|
7
|
+
"indiekit-plugin",
|
|
8
|
+
"indieweb",
|
|
9
|
+
"activitypub",
|
|
10
|
+
"fediverse",
|
|
11
|
+
"federation",
|
|
12
|
+
"fedify"
|
|
13
|
+
],
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Ricardo Mendes",
|
|
16
|
+
"url": "https://rmendes.net"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"exports": "./index.js",
|
|
25
|
+
"files": [
|
|
26
|
+
"assets",
|
|
27
|
+
"lib",
|
|
28
|
+
"locales",
|
|
29
|
+
"views",
|
|
30
|
+
"index.js"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/rmdes/indiekit-endpoint-activitypub"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/rmdes/indiekit-endpoint-activitypub/issues"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@fedify/fedify": "^1.10.0",
|
|
41
|
+
"@fedify/express": "^1.9.0",
|
|
42
|
+
"express": "^5.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@indiekit/error": "^1.0.0-beta.25",
|
|
46
|
+
"@indiekit/frontend": "^1.0.0-beta.25"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% from "heading/macro.njk" import heading with context %}
|
|
4
|
+
{% from "card/macro.njk" import card with context %}
|
|
5
|
+
{% from "badge/macro.njk" import badge with context %}
|
|
6
|
+
{% from "prose/macro.njk" import prose with context %}
|
|
7
|
+
{% from "pagination/macro.njk" import pagination with context %}
|
|
8
|
+
|
|
9
|
+
{% block content %}
|
|
10
|
+
{{ heading({ text: __("activitypub.activities"), level: 1, parent: { text: __("activitypub.title"), href: mountPath } }) }}
|
|
11
|
+
|
|
12
|
+
{% if activities.length > 0 %}
|
|
13
|
+
{% for activity in activities %}
|
|
14
|
+
{{ card({
|
|
15
|
+
title: activity.actorName or activity.actorUrl,
|
|
16
|
+
description: { text: activity.summary },
|
|
17
|
+
published: activity.receivedAt,
|
|
18
|
+
badges: [
|
|
19
|
+
{ text: activity.type },
|
|
20
|
+
{ text: __("activitypub.directionInbound") if activity.direction === "inbound" else __("activitypub.directionOutbound") }
|
|
21
|
+
]
|
|
22
|
+
}) }}
|
|
23
|
+
{% endfor %}
|
|
24
|
+
|
|
25
|
+
{{ pagination(cursor) if cursor }}
|
|
26
|
+
{% else %}
|
|
27
|
+
{{ prose({ text: __("activitypub.noActivity") }) }}
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% endblock %}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% from "heading/macro.njk" import heading with context %}
|
|
4
|
+
{% from "card/macro.njk" import card with context %}
|
|
5
|
+
{% from "card-grid/macro.njk" import cardGrid with context %}
|
|
6
|
+
{% from "prose/macro.njk" import prose with context %}
|
|
7
|
+
{% from "badge/macro.njk" import badge with context %}
|
|
8
|
+
|
|
9
|
+
{% block content %}
|
|
10
|
+
{{ heading({ text: title, level: 1 }) }}
|
|
11
|
+
|
|
12
|
+
{{ cardGrid({ cardSize: "16rem", items: [
|
|
13
|
+
{
|
|
14
|
+
title: followerCount + " " + __("activitypub.followers"),
|
|
15
|
+
url: mountPath + "/admin/followers"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: followingCount + " " + __("activitypub.following"),
|
|
19
|
+
url: mountPath + "/admin/following"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: __("activitypub.activities"),
|
|
23
|
+
url: mountPath + "/admin/activities"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: __("activitypub.migrate"),
|
|
27
|
+
url: mountPath + "/admin/migrate"
|
|
28
|
+
}
|
|
29
|
+
]}) }}
|
|
30
|
+
|
|
31
|
+
{{ heading({ text: __("activitypub.recentActivity"), level: 2 }) }}
|
|
32
|
+
|
|
33
|
+
{% if recentActivities.length > 0 %}
|
|
34
|
+
{% for activity in recentActivities %}
|
|
35
|
+
{{ card({
|
|
36
|
+
title: activity.actorName or activity.actorUrl,
|
|
37
|
+
description: { text: activity.summary },
|
|
38
|
+
published: activity.receivedAt,
|
|
39
|
+
badges: [{ text: activity.type }]
|
|
40
|
+
}) }}
|
|
41
|
+
{% endfor %}
|
|
42
|
+
{% else %}
|
|
43
|
+
{{ prose({ text: __("activitypub.noActivity") }) }}
|
|
44
|
+
{% endif %}
|
|
45
|
+
{% endblock %}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% from "heading/macro.njk" import heading with context %}
|
|
4
|
+
{% from "card/macro.njk" import card with context %}
|
|
5
|
+
{% from "prose/macro.njk" import prose with context %}
|
|
6
|
+
{% from "pagination/macro.njk" import pagination with context %}
|
|
7
|
+
|
|
8
|
+
{% block content %}
|
|
9
|
+
{{ heading({ text: followerCount + " " + __("activitypub.followers"), level: 1, parent: { text: __("activitypub.title"), href: mountPath } }) }}
|
|
10
|
+
|
|
11
|
+
{% if followers.length > 0 %}
|
|
12
|
+
{% for follower in followers %}
|
|
13
|
+
{{ card({
|
|
14
|
+
title: follower.name or follower.handle or follower.actorUrl,
|
|
15
|
+
url: follower.actorUrl,
|
|
16
|
+
photo: { src: follower.avatar, alt: follower.name } if follower.avatar,
|
|
17
|
+
description: { text: "@" + follower.handle if follower.handle },
|
|
18
|
+
published: follower.followedAt
|
|
19
|
+
}) }}
|
|
20
|
+
{% endfor %}
|
|
21
|
+
|
|
22
|
+
{{ pagination(cursor) if cursor }}
|
|
23
|
+
{% else %}
|
|
24
|
+
{{ prose({ text: __("activitypub.noFollowers") }) }}
|
|
25
|
+
{% endif %}
|
|
26
|
+
{% endblock %}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% from "heading/macro.njk" import heading with context %}
|
|
4
|
+
{% from "card/macro.njk" import card with context %}
|
|
5
|
+
{% from "prose/macro.njk" import prose with context %}
|
|
6
|
+
{% from "badge/macro.njk" import badge with context %}
|
|
7
|
+
{% from "pagination/macro.njk" import pagination with context %}
|
|
8
|
+
|
|
9
|
+
{% block content %}
|
|
10
|
+
{{ heading({ text: followingCount + " " + __("activitypub.following"), level: 1, parent: { text: __("activitypub.title"), href: mountPath } }) }}
|
|
11
|
+
|
|
12
|
+
{% if following.length > 0 %}
|
|
13
|
+
{% for account in following %}
|
|
14
|
+
{{ card({
|
|
15
|
+
title: account.name or account.handle or account.actorUrl,
|
|
16
|
+
url: account.actorUrl,
|
|
17
|
+
description: { text: "@" + account.handle if account.handle },
|
|
18
|
+
published: account.followedAt,
|
|
19
|
+
badges: [{ text: __("activitypub.sourceImport") if account.source === "import" else __("activitypub.sourceFederation") }]
|
|
20
|
+
}) }}
|
|
21
|
+
{% endfor %}
|
|
22
|
+
|
|
23
|
+
{{ pagination(cursor) if cursor }}
|
|
24
|
+
{% else %}
|
|
25
|
+
{{ prose({ text: __("activitypub.noFollowing") }) }}
|
|
26
|
+
{% endif %}
|
|
27
|
+
{% endblock %}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% from "heading/macro.njk" import heading with context %}
|
|
4
|
+
{% from "input/macro.njk" import input with context %}
|
|
5
|
+
{% from "button/macro.njk" import button with context %}
|
|
6
|
+
{% from "checkboxes/macro.njk" import checkboxes with context %}
|
|
7
|
+
{% from "file-input/macro.njk" import fileInput with context %}
|
|
8
|
+
{% from "details/macro.njk" import details with context %}
|
|
9
|
+
{% from "notification-banner/macro.njk" import notificationBanner with context %}
|
|
10
|
+
{% from "prose/macro.njk" import prose with context %}
|
|
11
|
+
|
|
12
|
+
{% block content %}
|
|
13
|
+
{{ heading({ text: title, level: 1, parent: { text: __("activitypub.title"), href: mountPath } }) }}
|
|
14
|
+
|
|
15
|
+
{% if result %}
|
|
16
|
+
{{ notificationBanner({ type: result.type, text: result.text }) }}
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
{# Step 1 — Actor alias #}
|
|
20
|
+
{{ heading({ text: __("activitypub.migrate.step1Title"), level: 2 }) }}
|
|
21
|
+
{{ prose({ text: __("activitypub.migrate.step1Desc") }) }}
|
|
22
|
+
|
|
23
|
+
<form method="post" novalidate>
|
|
24
|
+
<input type="hidden" name="action" value="alias">
|
|
25
|
+
{{ input({
|
|
26
|
+
name: "aliasUrl",
|
|
27
|
+
label: __("activitypub.migrate.aliasLabel"),
|
|
28
|
+
hint: __("activitypub.migrate.aliasHint"),
|
|
29
|
+
type: "url"
|
|
30
|
+
}) }}
|
|
31
|
+
{{ button({ text: __("activitypub.migrate.aliasSave") }) }}
|
|
32
|
+
</form>
|
|
33
|
+
|
|
34
|
+
<hr>
|
|
35
|
+
|
|
36
|
+
{# Step 2 — Import CSV #}
|
|
37
|
+
{{ heading({ text: __("activitypub.migrate.step2Title"), level: 2 }) }}
|
|
38
|
+
{{ prose({ text: __("activitypub.migrate.step2Desc") }) }}
|
|
39
|
+
|
|
40
|
+
<form method="post" enctype="multipart/form-data" novalidate>
|
|
41
|
+
<input type="hidden" name="action" value="import">
|
|
42
|
+
|
|
43
|
+
{{ checkboxes({
|
|
44
|
+
name: "importTypes",
|
|
45
|
+
items: [
|
|
46
|
+
{ value: "following", text: __("activitypub.migrate.importFollowing") },
|
|
47
|
+
{ value: "followers", text: __("activitypub.migrate.importFollowers") }
|
|
48
|
+
],
|
|
49
|
+
values: ["following"]
|
|
50
|
+
}) }}
|
|
51
|
+
|
|
52
|
+
{{ fileInput({
|
|
53
|
+
name: "csvFile",
|
|
54
|
+
label: __("activitypub.migrate.fileLabel"),
|
|
55
|
+
hint: __("activitypub.migrate.fileHint"),
|
|
56
|
+
accept: ".csv,.txt"
|
|
57
|
+
}) }}
|
|
58
|
+
|
|
59
|
+
{{ button({ text: __("activitypub.migrate.importButton") }) }}
|
|
60
|
+
</form>
|
|
61
|
+
|
|
62
|
+
<hr>
|
|
63
|
+
|
|
64
|
+
{# Step 3 — Instructions #}
|
|
65
|
+
{{ heading({ text: __("activitypub.migrate.step3Title"), level: 2 }) }}
|
|
66
|
+
{{ prose({ text: __("activitypub.migrate.step3Desc") }) }}
|
|
67
|
+
{% endblock %}
|