@rmdes/indiekit-endpoint-activitypub 2.6.2 → 2.7.1

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
@@ -976,34 +976,6 @@
976
976
  gap: var(--space-m);
977
977
  }
978
978
 
979
- .ap-compose__mode {
980
- border: var(--border-width-thin) solid var(--color-outline);
981
- border-radius: var(--border-radius-small);
982
- display: flex;
983
- flex-direction: column;
984
- gap: var(--space-s);
985
- padding: var(--space-m);
986
- }
987
-
988
- .ap-compose__mode legend {
989
- font-weight: 600;
990
- }
991
-
992
- .ap-compose__mode-option {
993
- cursor: pointer;
994
- display: flex;
995
- flex-wrap: wrap;
996
- gap: var(--space-xs);
997
- }
998
-
999
- .ap-compose__mode-hint {
1000
- color: var(--color-on-offset);
1001
- display: block;
1002
- font-size: var(--font-size-s);
1003
- margin-left: 1.5em;
1004
- width: 100%;
1005
- }
1006
-
1007
979
  .ap-compose__editor {
1008
980
  position: relative;
1009
981
  }
@@ -1027,21 +999,6 @@
1027
999
  outline-offset: -2px;
1028
1000
  }
1029
1001
 
1030
- .ap-compose__counter {
1031
- font-size: var(--font-size-s);
1032
- padding-top: var(--space-xs);
1033
- text-align: right;
1034
- }
1035
-
1036
- .ap-compose__counter--warn {
1037
- color: var(--color-yellow50);
1038
- }
1039
-
1040
- .ap-compose__counter--over {
1041
- color: var(--color-error);
1042
- font-weight: 600;
1043
- }
1044
-
1045
1002
  .ap-compose__syndication {
1046
1003
  border: var(--border-width-thin) solid var(--color-outline);
1047
1004
  border-radius: var(--border-radius-small);
@@ -2837,11 +2794,6 @@
2837
2794
  box-shadow: 0 2px 8px rgba(255, 255, 255, 0.06);
2838
2795
  }
2839
2796
 
2840
- /* --- Compose counter warning --- */
2841
- .ap-compose__counter--warn {
2842
- color: var(--color-yellow90);
2843
- }
2844
-
2845
2797
  /* --- Tab badge federated: soften purple --- */
2846
2798
  .ap-tab__badge--federated {
2847
2799
  color: var(--color-purple90);
package/index.js CHANGED
@@ -80,7 +80,6 @@ import { hashtagExploreApiController } from "./lib/controllers/hashtag-explore.j
80
80
  import { publicProfileController } from "./lib/controllers/public-profile.js";
81
81
  import { authorizeInteractionController } from "./lib/controllers/authorize-interaction.js";
82
82
  import { myProfileController } from "./lib/controllers/my-profile.js";
83
- import { noteObjectController } from "./lib/controllers/note-object.js";
84
83
  import {
85
84
  refollowPauseController,
86
85
  refollowResumeController,
@@ -189,10 +188,6 @@ export default class ActivityPubEndpoint {
189
188
  return self._fedifyMiddleware(req, res, next);
190
189
  });
191
190
 
192
- // Serve stored quick reply Notes as JSON-LD so remote servers can
193
- // dereference the Note ID during Create activity verification.
194
- router.get("/quick-replies/:id", noteObjectController(self));
195
-
196
191
  // Authorize interaction — remote follow / subscribe endpoint.
197
192
  // Remote servers redirect users here via the WebFinger subscribe template.
198
193
  router.get("/authorize_interaction", authorizeInteractionController(self));
@@ -889,7 +884,6 @@ export default class ActivityPubEndpoint {
889
884
  Indiekit.addCollection("ap_muted");
890
885
  Indiekit.addCollection("ap_blocked");
891
886
  Indiekit.addCollection("ap_interactions");
892
- Indiekit.addCollection("ap_notes");
893
887
  Indiekit.addCollection("ap_followed_tags");
894
888
  // Explore tab collections
895
889
  Indiekit.addCollection("ap_explore_tabs");
@@ -911,7 +905,6 @@ export default class ActivityPubEndpoint {
911
905
  ap_muted: indiekitCollections.get("ap_muted"),
912
906
  ap_blocked: indiekitCollections.get("ap_blocked"),
913
907
  ap_interactions: indiekitCollections.get("ap_interactions"),
914
- ap_notes: indiekitCollections.get("ap_notes"),
915
908
  ap_followed_tags: indiekitCollections.get("ap_followed_tags"),
916
909
  // Explore tab collections
917
910
  ap_explore_tabs: indiekitCollections.get("ap_explore_tabs"),
@@ -1,11 +1,9 @@
1
1
  /**
2
- * Compose controllers — reply form via Micropub or direct AP.
2
+ * Compose controllers — reply form via Micropub.
3
3
  */
4
4
 
5
- import { Temporal } from "@js-temporal/polyfill";
6
5
  import { getToken, validateToken } from "../csrf.js";
7
6
  import { sanitizeContent } from "../timeline-store.js";
8
- import { resolveAuthor } from "../resolve-author.js";
9
7
 
10
8
  /**
11
9
  * Fetch syndication targets from the Micropub config endpoint.
@@ -155,7 +153,7 @@ export function composeController(mountPath, plugin) {
155
153
  }
156
154
 
157
155
  /**
158
- * POST /admin/reader/compose — Submit reply via Micropub or direct AP.
156
+ * POST /admin/reader/compose — Submit reply via Micropub.
159
157
  * @param {string} mountPath - Plugin mount path
160
158
  * @param {object} plugin - ActivityPub plugin instance
161
159
  */
@@ -170,7 +168,7 @@ export function submitComposeController(mountPath, plugin) {
170
168
  }
171
169
 
172
170
  const { application } = request.app.locals;
173
- const { content, mode } = request.body;
171
+ const { content } = request.body;
174
172
  const inReplyTo = request.body["in-reply-to"];
175
173
  const syndicateTo = request.body["mp-syndicate-to"];
176
174
 
@@ -181,122 +179,7 @@ export function submitComposeController(mountPath, plugin) {
181
179
  });
182
180
  }
183
181
 
184
- // Quick reply direct AP
185
- if (mode === "quick") {
186
- if (!plugin._federation) {
187
- return response.status(503).render("error", {
188
- title: "Error",
189
- content: "Federation not initialized",
190
- });
191
- }
192
-
193
- const { Create, Note } = await import("@fedify/fedify/vocab");
194
- const handle = plugin.options.actor.handle;
195
- const ctx = plugin._federation.createContext(
196
- new URL(plugin._publicationUrl),
197
- { handle, publicationUrl: plugin._publicationUrl },
198
- );
199
-
200
- const uuid = crypto.randomUUID();
201
- const baseUrl = plugin._publicationUrl.replace(/\/$/, "");
202
- const noteId = `${baseUrl}/activitypub/quick-replies/${uuid}`;
203
- const actorUri = ctx.getActorUri(handle);
204
-
205
- const publicAddress = new URL(
206
- "https://www.w3.org/ns/activitystreams#Public",
207
- );
208
- const followersUri = ctx.getFollowersUri(handle);
209
-
210
- const documentLoader = await ctx.getDocumentLoader({
211
- identifier: handle,
212
- });
213
-
214
- // Resolve the original author BEFORE constructing the Note,
215
- // so we can include them in cc (required for threading/notification)
216
- let recipient = null;
217
- if (inReplyTo) {
218
- recipient = await resolveAuthor(
219
- inReplyTo,
220
- ctx,
221
- documentLoader,
222
- application?.collections,
223
- );
224
- }
225
-
226
- // Build cc list: always include followers, add original author for replies
227
- const ccList = [followersUri];
228
- if (recipient?.id) {
229
- ccList.push(recipient.id);
230
- }
231
-
232
- const note = new Note({
233
- id: new URL(noteId),
234
- attribution: actorUri,
235
- content: content.trim(),
236
- replyTarget: inReplyTo ? new URL(inReplyTo) : undefined,
237
- published: Temporal.Now.instant(),
238
- to: publicAddress,
239
- ccs: ccList,
240
- });
241
-
242
- const create = new Create({
243
- id: new URL(`${noteId}#activity`),
244
- actor: actorUri,
245
- object: note,
246
- to: publicAddress,
247
- ccs: ccList,
248
- });
249
-
250
- // Store the Note so remote servers can dereference its ID
251
- const ap_notes = application?.collections?.get("ap_notes");
252
- if (ap_notes) {
253
- await ap_notes.insertOne({
254
- _id: uuid,
255
- noteId,
256
- actorUrl: actorUri.href,
257
- content: content.trim(),
258
- inReplyTo: inReplyTo || null,
259
- published: new Date().toISOString(),
260
- to: ["https://www.w3.org/ns/activitystreams#Public"],
261
- cc: ccList.map((u) => (u instanceof URL ? u.href : u.href || u)),
262
- });
263
- }
264
-
265
- // Send to followers
266
- await ctx.sendActivity({ identifier: handle }, "followers", create, {
267
- preferSharedInbox: true,
268
- syncCollection: true,
269
- orderingKey: noteId,
270
- });
271
-
272
- // Also send directly to the original author's inbox
273
- if (recipient) {
274
- try {
275
- await ctx.sendActivity(
276
- { identifier: handle },
277
- recipient,
278
- create,
279
- { orderingKey: noteId },
280
- );
281
- console.info(
282
- `[ActivityPub] Sent quick reply directly to ${recipient.id?.href || "author"}`,
283
- );
284
- } catch (error) {
285
- console.warn(
286
- `[ActivityPub] Direct delivery to author failed (quick reply):`,
287
- error.message,
288
- );
289
- }
290
- }
291
-
292
- console.info(
293
- `[ActivityPub] Sent quick reply${inReplyTo ? ` to ${inReplyTo}` : ""}`,
294
- );
295
-
296
- return response.redirect(`${mountPath}/admin/reader`);
297
- }
298
-
299
- // Micropub path — post as blog reply
182
+ // Post as blog reply via Micropub
300
183
  const micropubEndpoint = application.micropubEndpoint;
301
184
 
302
185
  if (!micropubEndpoint) {
@@ -431,6 +431,7 @@ export function registerInboxListeners(inboxChain, options) {
431
431
 
432
432
  await addNotification(collections, {
433
433
  uid: object.id?.href || `reply:${actorUrl}:${inReplyTo}`,
434
+ url: object.url?.href || object.id?.href || "",
434
435
  type: "reply",
435
436
  actorUrl: actorInfo.url,
436
437
  actorName: actorInfo.name,
@@ -462,6 +463,7 @@ export function registerInboxListeners(inboxChain, options) {
462
463
 
463
464
  await addNotification(collections, {
464
465
  uid: object.id?.href || `mention:${actorUrl}:${object.id?.href}`,
466
+ url: object.url?.href || object.id?.href || "",
465
467
  type: "mention",
466
468
  actorUrl: actorInfo.url,
467
469
  actorName: actorInfo.name,
package/locales/en.json CHANGED
@@ -141,15 +141,9 @@
141
141
  },
142
142
  "compose": {
143
143
  "title": "Compose reply",
144
- "modeLabel": "Reply mode",
145
- "modeMicropub": "Post as blog reply",
146
- "modeMicropubHint": "Creates a permanent post on your blog, syndicated to the fediverse",
147
- "modeQuick": "Quick reply",
148
- "modeQuickHint": "Sends a reply directly to the fediverse (no blog post created)",
149
144
  "placeholder": "Write your reply…",
150
145
  "syndicateLabel": "Syndicate to",
151
146
  "submitMicropub": "Post reply",
152
- "submitQuick": "Send reply",
153
147
  "cancel": "Cancel",
154
148
  "errorEmpty": "Reply content cannot be empty"
155
149
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-activitypub",
3
- "version": "2.6.2",
3
+ "version": "2.7.1",
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",
@@ -21,50 +21,23 @@
21
21
  </div>
22
22
  {% endif %}
23
23
 
24
- <form method="post" action="{{ mountPath }}/admin/reader/compose" class="ap-compose__form"
25
- x-data="{
26
- mode: 'micropub',
27
- content: '',
28
- maxChars: 500,
29
- get remaining() { return this.maxChars - this.content.length; }
30
- }">
24
+ <form method="post" action="{{ mountPath }}/admin/reader/compose" class="ap-compose__form">
31
25
  <input type="hidden" name="_csrf" value="{{ csrfToken }}">
32
26
  {% if replyTo %}
33
27
  <input type="hidden" name="in-reply-to" value="{{ replyTo }}">
34
28
  {% endif %}
35
29
 
36
- {# Mode toggle #}
37
- <fieldset class="ap-compose__mode">
38
- <legend>{{ __("activitypub.compose.modeLabel") }}</legend>
39
- <label class="ap-compose__mode-option">
40
- <input type="radio" name="mode" value="micropub" x-model="mode" checked>
41
- {{ __("activitypub.compose.modeMicropub") }}
42
- <span class="ap-compose__mode-hint">{{ __("activitypub.compose.modeMicropubHint") }}</span>
43
- </label>
44
- <label class="ap-compose__mode-option">
45
- <input type="radio" name="mode" value="quick" x-model="mode">
46
- {{ __("activitypub.compose.modeQuick") }}
47
- <span class="ap-compose__mode-hint">{{ __("activitypub.compose.modeQuickHint") }}</span>
48
- </label>
49
- </fieldset>
50
-
51
30
  {# Content textarea #}
52
31
  <div class="ap-compose__editor">
53
32
  <textarea name="content" class="ap-compose__textarea"
54
33
  rows="6"
55
- :maxlength="mode === 'quick' ? maxChars : undefined"
56
- x-model="content"
57
34
  placeholder="{{ __('activitypub.compose.placeholder') }}"
58
35
  required></textarea>
59
- <div class="ap-compose__counter" x-show="mode === 'quick'" x-cloak>
60
- <span :class="{ 'ap-compose__counter--warn': remaining < 50, 'ap-compose__counter--over': remaining < 0 }"
61
- x-text="remaining"></span>
62
- </div>
63
36
  </div>
64
37
 
65
- {# Syndication targets (Micropub mode only) #}
38
+ {# Syndication targets #}
66
39
  {% if syndicationTargets.length > 0 %}
67
- <fieldset class="ap-compose__syndication" x-show="mode === 'micropub'">
40
+ <fieldset class="ap-compose__syndication">
68
41
  <legend>{{ __("activitypub.compose.syndicateLabel") }}</legend>
69
42
  {% for target in syndicationTargets %}
70
43
  <label class="ap-compose__syndication-target">
@@ -77,8 +50,7 @@
77
50
 
78
51
  <div class="ap-compose__actions">
79
52
  <button type="submit" class="ap-compose__submit">
80
- <span x-show="mode === 'micropub'">{{ __("activitypub.compose.submitMicropub") }}</span>
81
- <span x-show="mode === 'quick'">{{ __("activitypub.compose.submitQuick") }}</span>
53
+ {{ __("activitypub.compose.submitMicropub") }}
82
54
  </button>
83
55
  <a href="{{ mountPath }}/admin/reader" class="ap-compose__cancel">
84
56
  {{ __("activitypub.compose.cancel") }}
@@ -230,7 +230,7 @@
230
230
  if (this.error) setTimeout(() => this.error = '', 3000);
231
231
  }
232
232
  }">
233
- <a href="{{ mountPath }}/admin/reader/compose?replyTo={{ itemUid | urlencode }}"
233
+ <a href="{{ mountPath }}/admin/reader/compose?replyTo={{ (itemUrl or itemUid) | urlencode }}"
234
234
  class="ap-card__action ap-card__action--reply"
235
235
  title="{{ __('activitypub.reader.actions.reply') }}">
236
236
  ↩ {{ __("activitypub.reader.actions.reply") }}{% if replyCount != null %}<span class="ap-card__count">{{ replyCount }}</span>{% endif %}
@@ -56,10 +56,10 @@
56
56
 
57
57
  {% if item.type == "reply" or item.type == "mention" %}
58
58
  <div class="ap-notification__actions">
59
- <a href="{{ mountPath }}/admin/reader/compose?replyTo={{ item.uid | urlencode }}" class="ap-notification__reply-btn" title="{{ __('activitypub.reader.actions.reply') }}">
59
+ <a href="{{ mountPath }}/admin/reader/compose?replyTo={{ (item.url or item.uid) | urlencode }}" class="ap-notification__reply-btn" title="{{ __('activitypub.reader.actions.reply') }}">
60
60
  ↩ {{ __("activitypub.reader.actions.reply") }}
61
61
  </a>
62
- <a href="{{ mountPath }}/admin/reader/post?url={{ item.uid | urlencode }}" class="ap-notification__thread-btn" title="{{ __('activitypub.reader.post.title') }}">
62
+ <a href="{{ mountPath }}/admin/reader/post?url={{ (item.url or item.uid) | urlencode }}" class="ap-notification__thread-btn" title="{{ __('activitypub.reader.post.title') }}">
63
63
  💬 {{ __("activitypub.notifications.viewThread") }}
64
64
  </a>
65
65
  </div>
@@ -1,51 +0,0 @@
1
- /**
2
- * Public route handler for serving quick reply Notes as ActivityPub JSON-LD.
3
- *
4
- * Remote servers dereference Note IDs to verify Create activities.
5
- * Without this, quick replies are rejected by servers that validate
6
- * the Note's ID URL (Mastodon with Authorized Fetch, Bonfire, etc.).
7
- */
8
-
9
- /**
10
- * GET /quick-replies/:id — serve a stored Note as JSON-LD.
11
- * @param {object} plugin - ActivityPub plugin instance
12
- */
13
- export function noteObjectController(plugin) {
14
- return async (request, response) => {
15
- const { id } = request.params;
16
-
17
- const { application } = request.app.locals;
18
- const ap_notes = application?.collections?.get("ap_notes");
19
-
20
- if (!ap_notes) {
21
- return response.status(404).json({ error: "Not Found" });
22
- }
23
-
24
- const note = await ap_notes.findOne({ _id: id });
25
-
26
- if (!note) {
27
- return response.status(404).json({ error: "Not Found" });
28
- }
29
-
30
- const noteJson = {
31
- "@context": "https://www.w3.org/ns/activitystreams",
32
- id: note.noteId,
33
- type: "Note",
34
- attributedTo: note.actorUrl,
35
- content: note.content,
36
- published: note.published,
37
- to: note.to,
38
- cc: note.cc,
39
- };
40
-
41
- if (note.inReplyTo) {
42
- noteJson.inReplyTo = note.inReplyTo;
43
- }
44
-
45
- response
46
- .status(200)
47
- .set("Content-Type", "application/activity+json; charset=utf-8")
48
- .set("Cache-Control", "public, max-age=3600")
49
- .json(noteJson);
50
- };
51
- }