@rmdes/indiekit-endpoint-activitypub 2.10.1 → 2.11.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/index.js +60 -1
- package/lib/jf2-to-as2.js +98 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
jf2ToActivityStreams,
|
|
10
10
|
jf2ToAS2Activity,
|
|
11
|
+
parseMentions,
|
|
11
12
|
} from "./lib/jf2-to-as2.js";
|
|
12
13
|
import { dashboardController } from "./lib/controllers/dashboard.js";
|
|
13
14
|
import {
|
|
@@ -467,6 +468,40 @@ export default class ActivityPubEndpoint {
|
|
|
467
468
|
}
|
|
468
469
|
}
|
|
469
470
|
|
|
471
|
+
// Resolve @user@domain mentions in content via WebFinger
|
|
472
|
+
const contentText = properties.content?.html || properties.content || "";
|
|
473
|
+
const mentionHandles = parseMentions(contentText);
|
|
474
|
+
const resolvedMentions = [];
|
|
475
|
+
const mentionRecipients = [];
|
|
476
|
+
|
|
477
|
+
for (const { handle } of mentionHandles) {
|
|
478
|
+
try {
|
|
479
|
+
const mentionedActor = await ctx.lookupObject(
|
|
480
|
+
new URL(`acct:${handle}`),
|
|
481
|
+
);
|
|
482
|
+
if (mentionedActor?.id) {
|
|
483
|
+
resolvedMentions.push({
|
|
484
|
+
handle,
|
|
485
|
+
actorUrl: mentionedActor.id.href,
|
|
486
|
+
});
|
|
487
|
+
mentionRecipients.push({
|
|
488
|
+
handle,
|
|
489
|
+
actorUrl: mentionedActor.id.href,
|
|
490
|
+
actor: mentionedActor,
|
|
491
|
+
});
|
|
492
|
+
console.info(
|
|
493
|
+
`[ActivityPub] Resolved mention @${handle} → ${mentionedActor.id.href}`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
} catch (error) {
|
|
497
|
+
console.warn(
|
|
498
|
+
`[ActivityPub] Could not resolve mention @${handle}: ${error.message}`,
|
|
499
|
+
);
|
|
500
|
+
// Still add with no actorUrl so it gets a fallback link
|
|
501
|
+
resolvedMentions.push({ handle, actorUrl: null });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
470
505
|
const activity = jf2ToAS2Activity(
|
|
471
506
|
properties,
|
|
472
507
|
actorUrl,
|
|
@@ -475,6 +510,7 @@ export default class ActivityPubEndpoint {
|
|
|
475
510
|
replyToActorUrl: replyToActor?.url,
|
|
476
511
|
replyToActorHandle: replyToActor?.handle,
|
|
477
512
|
visibility: self.options.defaultVisibility,
|
|
513
|
+
mentions: resolvedMentions,
|
|
478
514
|
},
|
|
479
515
|
);
|
|
480
516
|
|
|
@@ -529,12 +565,35 @@ export default class ActivityPubEndpoint {
|
|
|
529
565
|
}
|
|
530
566
|
}
|
|
531
567
|
|
|
568
|
+
// Deliver to mentioned actors' inboxes (skip reply-to author, already delivered above)
|
|
569
|
+
for (const { handle: mHandle, actorUrl: mUrl, actor: mActor } of mentionRecipients) {
|
|
570
|
+
if (replyToActor?.url === mUrl) continue;
|
|
571
|
+
try {
|
|
572
|
+
await ctx.sendActivity(
|
|
573
|
+
{ identifier: handle },
|
|
574
|
+
mActor,
|
|
575
|
+
activity,
|
|
576
|
+
{ orderingKey: properties.url },
|
|
577
|
+
);
|
|
578
|
+
console.info(
|
|
579
|
+
`[ActivityPub] Mention delivered to @${mHandle}: ${mUrl}`,
|
|
580
|
+
);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
console.warn(
|
|
583
|
+
`[ActivityPub] Failed to deliver mention to @${mHandle}: ${error.message}`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
532
588
|
// Determine activity type name
|
|
533
589
|
const typeName =
|
|
534
590
|
activity.constructor?.name || "Create";
|
|
535
591
|
const replyNote = replyToActor
|
|
536
592
|
? ` (reply to ${replyToActor.url})`
|
|
537
593
|
: "";
|
|
594
|
+
const mentionNote = mentionRecipients.length > 0
|
|
595
|
+
? ` (mentions: ${mentionRecipients.map(m => `@${m.handle}`).join(", ")})`
|
|
596
|
+
: "";
|
|
538
597
|
|
|
539
598
|
await logActivity(self._collections.ap_activities, {
|
|
540
599
|
direction: "outbound",
|
|
@@ -542,7 +601,7 @@ export default class ActivityPubEndpoint {
|
|
|
542
601
|
actorUrl: self._publicationUrl,
|
|
543
602
|
objectUrl: properties.url,
|
|
544
603
|
targetUrl: properties["in-reply-to"] || undefined,
|
|
545
|
-
summary: `Sent ${typeName} for ${properties.url} to ${followerCount} followers${replyNote}`,
|
|
604
|
+
summary: `Sent ${typeName} for ${properties.url} to ${followerCount} followers${replyNote}${mentionNote}`,
|
|
546
605
|
});
|
|
547
606
|
|
|
548
607
|
console.info(
|
package/lib/jf2-to-as2.js
CHANGED
|
@@ -36,6 +36,50 @@ function linkifyUrls(html) {
|
|
|
36
36
|
);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Parse @user@domain mention patterns from text content.
|
|
41
|
+
* Returns array of { handle: "user@domain", username: "user", domain: "domain.tld" }.
|
|
42
|
+
*/
|
|
43
|
+
export function parseMentions(text) {
|
|
44
|
+
if (!text) return [];
|
|
45
|
+
// Strip HTML tags for parsing
|
|
46
|
+
const plain = text.replace(/<[^>]*>/g, " ");
|
|
47
|
+
const mentionRegex = /(?<![\/\w])@([\w.-]+)@([\w.-]+\.\w{2,})/g;
|
|
48
|
+
const mentions = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = mentionRegex.exec(plain)) !== null) {
|
|
52
|
+
const handle = `${match[1]}@${match[2]}`;
|
|
53
|
+
if (!seen.has(handle.toLowerCase())) {
|
|
54
|
+
seen.add(handle.toLowerCase());
|
|
55
|
+
mentions.push({ handle, username: match[1], domain: match[2] });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return mentions;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Replace @user@domain patterns in HTML with linked mentions.
|
|
63
|
+
* resolvedMentions: [{ handle, actorUrl }]
|
|
64
|
+
* Unresolved handles get a WebFinger-style link as fallback.
|
|
65
|
+
*/
|
|
66
|
+
function linkifyMentions(html, resolvedMentions) {
|
|
67
|
+
if (!html || !resolvedMentions?.length) return html;
|
|
68
|
+
for (const { handle, actorUrl } of resolvedMentions) {
|
|
69
|
+
// Escape handle for regex (dots, hyphens)
|
|
70
|
+
const escaped = handle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
71
|
+
// Match @handle not already inside an HTML tag attribute or anchor text
|
|
72
|
+
const pattern = new RegExp(`(?<!["\\/\\w])@${escaped}(?![\\w])`, "gi");
|
|
73
|
+
const parts = handle.split("@");
|
|
74
|
+
const url = actorUrl || `https://${parts[1]}/@${parts[0]}`;
|
|
75
|
+
html = html.replace(
|
|
76
|
+
pattern,
|
|
77
|
+
`<a href="${url}" class="mention" rel="nofollow noopener" target="_blank">@${handle}</a>`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return html;
|
|
81
|
+
}
|
|
82
|
+
|
|
39
83
|
// ---------------------------------------------------------------------------
|
|
40
84
|
// Plain JSON-LD (content negotiation on individual post URLs)
|
|
41
85
|
// ---------------------------------------------------------------------------
|
|
@@ -137,10 +181,27 @@ export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, optio
|
|
|
137
181
|
}
|
|
138
182
|
|
|
139
183
|
const tags = buildPlainTags(properties, publicationUrl, object.tag);
|
|
184
|
+
|
|
185
|
+
// Add Mention tags + cc addressing + content linkification for @mentions
|
|
186
|
+
const resolvedMentions = options.mentions || [];
|
|
187
|
+
for (const { handle, actorUrl: mentionUrl } of resolvedMentions) {
|
|
188
|
+
if (mentionUrl) {
|
|
189
|
+
tags.push({ type: "Mention", href: mentionUrl, name: `@${handle}` });
|
|
190
|
+
if (!object.cc.includes(mentionUrl)) {
|
|
191
|
+
object.cc.push(mentionUrl);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
140
196
|
if (tags.length > 0) {
|
|
141
197
|
object.tag = tags;
|
|
142
198
|
}
|
|
143
199
|
|
|
200
|
+
// Linkify @mentions in content (resolved get actor links, unresolved get profile links)
|
|
201
|
+
if (resolvedMentions.length > 0 && object.content) {
|
|
202
|
+
object.content = linkifyMentions(object.content, resolvedMentions);
|
|
203
|
+
}
|
|
204
|
+
|
|
144
205
|
return {
|
|
145
206
|
"@context": "https://www.w3.org/ns/activitystreams",
|
|
146
207
|
type: "Create",
|
|
@@ -292,7 +353,7 @@ export function jf2ToAS2Activity(properties, actorUrl, publicationUrl, options =
|
|
|
292
353
|
noteOptions.attachments = fedifyAttachments;
|
|
293
354
|
}
|
|
294
355
|
|
|
295
|
-
// Tags: hashtags + Mention for reply addressing
|
|
356
|
+
// Tags: hashtags + Mention for reply addressing + @mentions
|
|
296
357
|
const fedifyTags = buildFedifyTags(properties, publicationUrl, postType);
|
|
297
358
|
|
|
298
359
|
if (replyToActorUrl) {
|
|
@@ -304,10 +365,46 @@ export function jf2ToAS2Activity(properties, actorUrl, publicationUrl, options =
|
|
|
304
365
|
);
|
|
305
366
|
}
|
|
306
367
|
|
|
368
|
+
// Add Mention tags + cc addressing for resolved @mentions
|
|
369
|
+
const resolvedMentions = options.mentions || [];
|
|
370
|
+
const ccUrls = [];
|
|
371
|
+
for (const { handle, actorUrl: mentionUrl } of resolvedMentions) {
|
|
372
|
+
if (mentionUrl) {
|
|
373
|
+
// Skip if same as replyToActorUrl (already added above)
|
|
374
|
+
const alreadyTagged = replyToActorUrl && mentionUrl === replyToActorUrl;
|
|
375
|
+
if (!alreadyTagged) {
|
|
376
|
+
fedifyTags.push(
|
|
377
|
+
new Mention({
|
|
378
|
+
href: new URL(mentionUrl),
|
|
379
|
+
name: `@${handle}`,
|
|
380
|
+
}),
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
ccUrls.push(new URL(mentionUrl));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Merge mention actors into cc/ccs
|
|
388
|
+
if (ccUrls.length > 0) {
|
|
389
|
+
if (noteOptions.ccs) {
|
|
390
|
+
noteOptions.ccs = [...noteOptions.ccs, ...ccUrls];
|
|
391
|
+
} else if (noteOptions.cc) {
|
|
392
|
+
noteOptions.ccs = [noteOptions.cc, ...ccUrls];
|
|
393
|
+
delete noteOptions.cc;
|
|
394
|
+
} else {
|
|
395
|
+
noteOptions.ccs = ccUrls;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
307
399
|
if (fedifyTags.length > 0) {
|
|
308
400
|
noteOptions.tags = fedifyTags;
|
|
309
401
|
}
|
|
310
402
|
|
|
403
|
+
// Linkify @mentions in content
|
|
404
|
+
if (resolvedMentions.length > 0 && noteOptions.content) {
|
|
405
|
+
noteOptions.content = linkifyMentions(noteOptions.content, resolvedMentions);
|
|
406
|
+
}
|
|
407
|
+
|
|
311
408
|
const object = isArticle
|
|
312
409
|
? new Article(noteOptions)
|
|
313
410
|
: new Note(noteOptions);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
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",
|