feat: visibility/CW compose controls, @mention support (v2.11.0)

Add visibility and content warning controls to the reply compose form.
Add @user@domain mention parsing, WebFinger resolution, Mention tags,
inbox delivery, and content linkification for outbound posts.

Confab-Link: http://localhost:8080/sessions/cc343b15-8d10-43cd-a48f-ca912eb79b83
This commit is contained in:
Ricardo
2026-03-14 21:28:24 +01:00
parent 1dc42ad5e5
commit 6238e7d4e5
7 changed files with 258 additions and 5 deletions

View File

@@ -8,6 +8,7 @@ import {
import {
jf2ToActivityStreams,
jf2ToAS2Activity,
parseMentions,
} from "./lib/jf2-to-as2.js";
import { dashboardController } from "./lib/controllers/dashboard.js";
import {
@@ -467,6 +468,40 @@ export default class ActivityPubEndpoint {
}
}
// Resolve @user@domain mentions in content via WebFinger
const contentText = properties.content?.html || properties.content || "";
const mentionHandles = parseMentions(contentText);
const resolvedMentions = [];
const mentionRecipients = [];
for (const { handle } of mentionHandles) {
try {
const mentionedActor = await ctx.lookupObject(
new URL(`acct:${handle}`),
);
if (mentionedActor?.id) {
resolvedMentions.push({
handle,
actorUrl: mentionedActor.id.href,
});
mentionRecipients.push({
handle,
actorUrl: mentionedActor.id.href,
actor: mentionedActor,
});
console.info(
`[ActivityPub] Resolved mention @${handle}${mentionedActor.id.href}`,
);
}
} catch (error) {
console.warn(
`[ActivityPub] Could not resolve mention @${handle}: ${error.message}`,
);
// Still add with no actorUrl so it gets a fallback link
resolvedMentions.push({ handle, actorUrl: null });
}
}
const activity = jf2ToAS2Activity(
properties,
actorUrl,
@@ -475,6 +510,7 @@ export default class ActivityPubEndpoint {
replyToActorUrl: replyToActor?.url,
replyToActorHandle: replyToActor?.handle,
visibility: self.options.defaultVisibility,
mentions: resolvedMentions,
},
);
@@ -529,12 +565,35 @@ export default class ActivityPubEndpoint {
}
}
// Deliver to mentioned actors' inboxes (skip reply-to author, already delivered above)
for (const { handle: mHandle, actorUrl: mUrl, actor: mActor } of mentionRecipients) {
if (replyToActor?.url === mUrl) continue;
try {
await ctx.sendActivity(
{ identifier: handle },
mActor,
activity,
{ orderingKey: properties.url },
);
console.info(
`[ActivityPub] Mention delivered to @${mHandle}: ${mUrl}`,
);
} catch (error) {
console.warn(
`[ActivityPub] Failed to deliver mention to @${mHandle}: ${error.message}`,
);
}
}
// Determine activity type name
const typeName =
activity.constructor?.name || "Create";
const replyNote = replyToActor
? ` (reply to ${replyToActor.url})`
: "";
const mentionNote = mentionRecipients.length > 0
? ` (mentions: ${mentionRecipients.map(m => `@${m.handle}`).join(", ")})`
: "";
await logActivity(self._collections.ap_activities, {
direction: "outbound",
@@ -542,7 +601,7 @@ export default class ActivityPubEndpoint {
actorUrl: self._publicationUrl,
objectUrl: properties.url,
targetUrl: properties["in-reply-to"] || undefined,
summary: `Sent ${typeName} for ${properties.url} to ${followerCount} followers${replyNote}`,
summary: `Sent ${typeName} for ${properties.url} to ${followerCount} followers${replyNote}${mentionNote}`,
});
console.info(