@rmdes/indiekit-endpoint-activitypub 1.1.16 → 1.1.17

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/reader.css CHANGED
@@ -4,6 +4,54 @@
4
4
  * Uses Indiekit CSS custom properties for automatic dark mode support
5
5
  */
6
6
 
7
+ /* ==========================================================================
8
+ Fediverse Lookup
9
+ ========================================================================== */
10
+
11
+ .ap-lookup {
12
+ display: flex;
13
+ gap: var(--space-xs);
14
+ margin-bottom: var(--space-m);
15
+ }
16
+
17
+ .ap-lookup__input {
18
+ flex: 1;
19
+ padding: var(--space-s) var(--space-m);
20
+ border: var(--border-width-thin) solid var(--color-outline);
21
+ border-radius: var(--border-radius-small);
22
+ background: var(--color-offset);
23
+ color: var(--color-on-background);
24
+ font-size: var(--font-size-m);
25
+ font-family: inherit;
26
+ }
27
+
28
+ .ap-lookup__input::placeholder {
29
+ color: var(--color-on-offset);
30
+ }
31
+
32
+ .ap-lookup__input:focus {
33
+ outline: 2px solid var(--color-primary);
34
+ outline-offset: -1px;
35
+ border-color: var(--color-primary);
36
+ }
37
+
38
+ .ap-lookup__btn {
39
+ padding: var(--space-s) var(--space-m);
40
+ border: var(--border-width-thin) solid var(--color-primary);
41
+ border-radius: var(--border-radius-small);
42
+ background: var(--color-primary);
43
+ color: var(--color-on-primary);
44
+ font-size: var(--font-size-m);
45
+ font-family: inherit;
46
+ font-weight: 600;
47
+ cursor: pointer;
48
+ white-space: nowrap;
49
+ }
50
+
51
+ .ap-lookup__btn:hover {
52
+ opacity: 0.9;
53
+ }
54
+
7
55
  /* ==========================================================================
8
56
  Tab Navigation
9
57
  ========================================================================== */
package/index.js CHANGED
@@ -57,6 +57,7 @@ import {
57
57
  featuredTagsAddController,
58
58
  featuredTagsRemoveController,
59
59
  } from "./lib/controllers/featured-tags.js";
60
+ import { resolveController } from "./lib/controllers/resolve.js";
60
61
  import {
61
62
  refollowPauseController,
62
63
  refollowResumeController,
@@ -202,6 +203,7 @@ export default class ActivityPubEndpoint {
202
203
  router.post("/admin/reader/unlike", unlikeController(mp, this));
203
204
  router.post("/admin/reader/boost", boostController(mp, this));
204
205
  router.post("/admin/reader/unboost", unboostController(mp, this));
206
+ router.get("/admin/reader/resolve", resolveController(mp, this));
205
207
  router.get("/admin/reader/profile", remoteProfileController(mp, this));
206
208
  router.get("/admin/reader/post", postDetailController(mp, this));
207
209
  router.post("/admin/reader/follow", followController(mp, this));
@@ -32,7 +32,7 @@ export function followersController(mountPath) {
32
32
  const cursor = buildCursor(page, totalPages, mountPath + "/admin/followers");
33
33
 
34
34
  response.render("activitypub-followers", {
35
- title: response.locals.__("activitypub.followers"),
35
+ title: `${totalCount} ${response.locals.__("activitypub.followers")}`,
36
36
  followers,
37
37
  followerCount: totalCount,
38
38
  mountPath,
@@ -32,7 +32,7 @@ export function followingController(mountPath) {
32
32
  const cursor = buildCursor(page, totalPages, mountPath + "/admin/following");
33
33
 
34
34
  response.render("activitypub-following", {
35
- title: response.locals.__("activitypub.following"),
35
+ title: `${totalCount} ${response.locals.__("activitypub.following")}`,
36
36
  following,
37
37
  followingCount: totalCount,
38
38
  mountPath,
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Resolve controller — accepts any fediverse URL or handle, resolves it
3
+ * via lookupObject(), and redirects to the appropriate internal view.
4
+ */
5
+ import {
6
+ Article,
7
+ Note,
8
+ Person,
9
+ Service,
10
+ Application,
11
+ Organization,
12
+ Group,
13
+ } from "@fedify/fedify";
14
+
15
+ /**
16
+ * GET /admin/reader/resolve?q=<url-or-handle>
17
+ * Resolves a fediverse URL or @user@domain handle and redirects to
18
+ * the post detail or remote profile view.
19
+ */
20
+ export function resolveController(mountPath, plugin) {
21
+ return async (request, response, next) => {
22
+ try {
23
+ const query = (request.query.q || "").trim();
24
+
25
+ if (!query) {
26
+ return response.redirect(`${mountPath}/admin/reader`);
27
+ }
28
+
29
+ if (!plugin._federation) {
30
+ return response.status(503).render("error", {
31
+ title: "Error",
32
+ content: "Federation not initialized",
33
+ });
34
+ }
35
+
36
+ const handle = plugin.options.actor.handle;
37
+ const ctx = plugin._federation.createContext(
38
+ new URL(plugin._publicationUrl),
39
+ { handle, publicationUrl: plugin._publicationUrl },
40
+ );
41
+
42
+ const documentLoader = await ctx.getDocumentLoader({
43
+ identifier: handle,
44
+ });
45
+
46
+ // Determine if input is a URL or a handle
47
+ // lookupObject accepts: URLs, @user@domain, user@domain, acct:user@domain
48
+ let lookupInput;
49
+
50
+ try {
51
+ // If it parses as a URL, pass as URL object
52
+ const parsed = new URL(query);
53
+ lookupInput = parsed;
54
+ } catch {
55
+ // Not a URL — treat as handle (strip leading @ if present)
56
+ lookupInput = query;
57
+ }
58
+
59
+ let object;
60
+
61
+ try {
62
+ object = await ctx.lookupObject(lookupInput, { documentLoader });
63
+ } catch (error) {
64
+ console.warn(
65
+ `[resolve] lookupObject failed for "${query}":`,
66
+ error.message,
67
+ );
68
+ }
69
+
70
+ if (!object) {
71
+ return response.status(404).render("error", {
72
+ title: response.locals.__("activitypub.reader.resolve.notFoundTitle"),
73
+ content: response.locals.__(
74
+ "activitypub.reader.resolve.notFound",
75
+ ),
76
+ });
77
+ }
78
+
79
+ // Determine object type and redirect accordingly
80
+ const objectUrl =
81
+ object.id?.href || object.url?.href || query;
82
+
83
+ if (
84
+ object instanceof Person ||
85
+ object instanceof Service ||
86
+ object instanceof Application ||
87
+ object instanceof Organization ||
88
+ object instanceof Group
89
+ ) {
90
+ return response.redirect(
91
+ `${mountPath}/admin/reader/profile?url=${encodeURIComponent(objectUrl)}`,
92
+ );
93
+ }
94
+
95
+ if (object instanceof Note || object instanceof Article) {
96
+ return response.redirect(
97
+ `${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
98
+ );
99
+ }
100
+
101
+ // Unknown type — try post detail as fallback
102
+ return response.redirect(
103
+ `${mountPath}/admin/reader/post?url=${encodeURIComponent(objectUrl)}`,
104
+ );
105
+ } catch (error) {
106
+ next(error);
107
+ }
108
+ };
109
+ }
package/locales/en.json CHANGED
@@ -191,6 +191,13 @@
191
191
  "loadingThread": "Loading thread...",
192
192
  "threadError": "Could not load full thread"
193
193
  },
194
+ "resolve": {
195
+ "placeholder": "Paste a fediverse URL or @user@domain handle…",
196
+ "label": "Look up a fediverse post or account",
197
+ "button": "Look up",
198
+ "notFoundTitle": "Not found",
199
+ "notFound": "Could not find this post or account. The URL may be invalid, the server may be unavailable, or the content may have been deleted."
200
+ },
194
201
  "linkPreview": {
195
202
  "label": "Link preview"
196
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "1.1.16",
3
+ "version": "1.1.17",
4
4
  "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -7,8 +7,6 @@
7
7
  {% from "pagination/macro.njk" import pagination with context %}
8
8
 
9
9
  {% block content %}
10
- {{ heading({ text: __("activitypub.activities"), level: 1 }) }}
11
-
12
10
  {% if activities.length > 0 %}
13
11
  {% for activity in activities %}
14
12
  {{ card({
@@ -3,8 +3,6 @@
3
3
  {% from "heading/macro.njk" import heading with context %}
4
4
 
5
5
  {% block readercontent %}
6
- {{ heading({ text: title, level: 1 }) }}
7
-
8
6
  {# Reply context — show the post being replied to #}
9
7
  {% if replyContext %}
10
8
  <div class="ap-compose__context">
@@ -7,8 +7,6 @@
7
7
  {% from "badge/macro.njk" import badge with context %}
8
8
 
9
9
  {% block content %}
10
- {{ heading({ text: title, level: 1 }) }}
11
-
12
10
  {{ cardGrid({ cardSize: "16rem", items: [
13
11
  {
14
12
  title: followerCount + " " + __("activitypub.followers"),
@@ -4,8 +4,6 @@
4
4
  {% from "prose/macro.njk" import prose with context %}
5
5
 
6
6
  {% block content %}
7
- {{ heading({ text: title, level: 1 }) }}
8
-
9
7
  {% if tags.length > 0 %}
10
8
  <table class="table">
11
9
  <thead>
@@ -4,8 +4,6 @@
4
4
  {% from "prose/macro.njk" import prose with context %}
5
5
 
6
6
  {% block content %}
7
- {{ heading({ text: title, level: 1 }) }}
8
-
9
7
  {% if pinned.length > 0 %}
10
8
  <table class="table">
11
9
  <thead>
@@ -6,8 +6,6 @@
6
6
  {% from "pagination/macro.njk" import pagination with context %}
7
7
 
8
8
  {% block content %}
9
- {{ heading({ text: followerCount + " " + __("activitypub.followers"), level: 1 }) }}
10
-
11
9
  {% if followers.length > 0 %}
12
10
  {% for follower in followers %}
13
11
  {{ card({
@@ -7,8 +7,6 @@
7
7
  {% from "pagination/macro.njk" import pagination with context %}
8
8
 
9
9
  {% block content %}
10
- {{ heading({ text: followingCount + " " + __("activitypub.following"), level: 1 }) }}
11
-
12
10
  {% if following.length > 0 %}
13
11
  {% for account in following %}
14
12
  {% if account.source === "import" %}
@@ -8,8 +8,6 @@
8
8
  {% from "prose/macro.njk" import prose with context %}
9
9
 
10
10
  {% block content %}
11
- {{ heading({ text: title, level: 1 }) }}
12
-
13
11
  {% if result %}
14
12
  {{ notificationBanner({ type: result.type, text: result.text }) }}
15
13
  {% endif %}
@@ -4,8 +4,6 @@
4
4
  {% from "prose/macro.njk" import prose with context %}
5
5
 
6
6
  {% block readercontent %}
7
- {{ heading({ text: title, level: 1 }) }}
8
-
9
7
  {# Blocked actors #}
10
8
  <section class="ap-moderation__section">
11
9
  <h2>{{ __("activitypub.moderation.blockedTitle") }}</h2>
@@ -4,8 +4,6 @@
4
4
  {% from "prose/macro.njk" import prose with context %}
5
5
 
6
6
  {% block readercontent %}
7
- {{ heading({ text: __("activitypub.notifications.title"), level: 1 }) }}
8
-
9
7
  {% if items.length > 0 %}
10
8
  <div class="ap-notifications__toolbar">
11
9
  {% if unreadCount > 0 %}
@@ -3,8 +3,6 @@
3
3
  {% from "heading/macro.njk" import heading with context %}
4
4
 
5
5
  {% block readercontent %}
6
- {{ heading({ text: title, level: 1 }) }}
7
-
8
6
  <div class="ap-post-detail" data-mount-path="{{ mountPath }}">
9
7
  {# Back button #}
10
8
  <div class="ap-post-detail__back">
@@ -10,8 +10,6 @@
10
10
  {% from "prose/macro.njk" import prose with context %}
11
11
 
12
12
  {% block content %}
13
- {{ heading({ text: title, level: 1 }) }}
14
-
15
13
  {% if result %}
16
14
  {{ notificationBanner({ type: result.type, text: result.text }) }}
17
15
  {% endif %}
@@ -4,7 +4,13 @@
4
4
  {% from "prose/macro.njk" import prose with context %}
5
5
 
6
6
  {% block readercontent %}
7
- {{ heading({ text: __("activitypub.reader.title"), level: 1 }) }}
7
+ {# Fediverse lookup #}
8
+ <form action="{{ mountPath }}/admin/reader/resolve" method="get" class="ap-lookup">
9
+ <input type="text" name="q" class="ap-lookup__input"
10
+ placeholder="{{ __('activitypub.reader.resolve.placeholder') }}"
11
+ aria-label="{{ __('activitypub.reader.resolve.label') }}">
12
+ <button type="submit" class="ap-lookup__btn">{{ __("activitypub.reader.resolve.button") }}</button>
13
+ </form>
8
14
 
9
15
  {# Tab navigation #}
10
16
  <nav class="ap-tabs" role="tablist">
@@ -4,8 +4,6 @@
4
4
  {% from "prose/macro.njk" import prose with context %}
5
5
 
6
6
  {% block readercontent %}
7
- {{ heading({ text: title, level: 1 }) }}
8
-
9
7
  <div class="ap-profile"
10
8
  x-data="{
11
9
  following: {{ 'true' if isFollowing else 'false' }},