feat(like): send Like activity for AP objects, bookmark for regular URLs
When the `like-of` URL serves ActivityPub content (detected via content
negotiation with Accept: application/activity+json), deliver a proper
`Like { actor, object, to: Public }` activity to followers.
For likes of regular (non-AP) URLs, fall through to the existing
bookmark-style `Create(Note)` behaviour (🔖 content with #bookmark tag).
- Add `isApUrl()` async helper (3 s timeout, fails silently)
- Make `jf2ToAS2Activity` async; add Like detection before repost block
- Update all four call sites in federation-setup.js and index.js
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -583,7 +583,7 @@ export default class ActivityPubEndpoint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const activity = jf2ToAS2Activity(
|
const activity = await jf2ToAS2Activity(
|
||||||
properties,
|
properties,
|
||||||
actorUrl,
|
actorUrl,
|
||||||
self._publicationUrl,
|
self._publicationUrl,
|
||||||
|
|||||||
@@ -560,7 +560,7 @@ function setupFeatured(federation, mountPath, handle, collections, publicationUr
|
|||||||
});
|
});
|
||||||
if (!post) continue;
|
if (!post) continue;
|
||||||
const actorUrl = ctx.getActorUri(identifier).href;
|
const actorUrl = ctx.getActorUri(identifier).href;
|
||||||
const activity = jf2ToAS2Activity(
|
const activity = await jf2ToAS2Activity(
|
||||||
post.properties,
|
post.properties,
|
||||||
actorUrl,
|
actorUrl,
|
||||||
publicationUrl,
|
publicationUrl,
|
||||||
@@ -637,10 +637,11 @@ function setupOutbox(federation, mountPath, handle, collections) {
|
|||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
const { jf2ToAS2Activity } = await import("./jf2-to-as2.js");
|
const { jf2ToAS2Activity } = await import("./jf2-to-as2.js");
|
||||||
const items = posts
|
const items = (
|
||||||
.map((post) => {
|
await Promise.all(
|
||||||
|
posts.map(async (post) => {
|
||||||
try {
|
try {
|
||||||
return jf2ToAS2Activity(
|
return await jf2ToAS2Activity(
|
||||||
post.properties,
|
post.properties,
|
||||||
ctx.getActorUri(identifier).href,
|
ctx.getActorUri(identifier).href,
|
||||||
collections._publicationUrl,
|
collections._publicationUrl,
|
||||||
@@ -648,8 +649,9 @@ function setupOutbox(federation, mountPath, handle, collections) {
|
|||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
.filter(Boolean);
|
)
|
||||||
|
).filter(Boolean);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
@@ -687,7 +689,7 @@ function setupObjectDispatchers(federation, mountPath, handle, collections, publ
|
|||||||
// Soft-deleted posts should not be dereferenceable
|
// Soft-deleted posts should not be dereferenceable
|
||||||
if (post.properties?.deleted) return null;
|
if (post.properties?.deleted) return null;
|
||||||
const actorUrl = ctx.getActorUri(handle).href;
|
const actorUrl = ctx.getActorUri(handle).href;
|
||||||
const activity = jf2ToAS2Activity(post.properties, actorUrl, publicationUrl);
|
const activity = await jf2ToAS2Activity(post.properties, actorUrl, publicationUrl);
|
||||||
// Only Create activities wrap Note/Article objects
|
// Only Create activities wrap Note/Article objects
|
||||||
if (!(activity instanceof Create)) return null;
|
if (!(activity instanceof Create)) return null;
|
||||||
return await activity.getObject();
|
return await activity.getObject();
|
||||||
|
|||||||
+39
-3
@@ -14,6 +14,7 @@ import {
|
|||||||
Create,
|
Create,
|
||||||
Hashtag,
|
Hashtag,
|
||||||
Image,
|
Image,
|
||||||
|
Like,
|
||||||
Mention,
|
Mention,
|
||||||
Note,
|
Note,
|
||||||
Video,
|
Video,
|
||||||
@@ -79,6 +80,30 @@ function linkifyMentions(html, resolvedMentions) {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ActivityPub URL detection
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a URL serves ActivityPub content by doing a quick content
|
||||||
|
* negotiation request. Returns true if the server responds with an AP
|
||||||
|
* media type (application/activity+json or application/ld+json).
|
||||||
|
* Fails silently — any network/timeout error returns false.
|
||||||
|
*/
|
||||||
|
async function isApUrl(url) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { Accept: "application/activity+json, application/ld+json" },
|
||||||
|
redirect: "follow",
|
||||||
|
signal: AbortSignal.timeout(3000),
|
||||||
|
});
|
||||||
|
const ct = res.headers.get("content-type") || "";
|
||||||
|
return ct.includes("activity+json") || ct.includes("ld+json");
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Plain JSON-LD (content negotiation on individual post URLs)
|
// Plain JSON-LD (content negotiation on individual post URLs)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -239,13 +264,24 @@ export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, optio
|
|||||||
* @param {object} [options] - Optional settings
|
* @param {object} [options] - Optional settings
|
||||||
* @param {string} [options.replyToActorUrl] - Original post author's actor URL (for reply addressing)
|
* @param {string} [options.replyToActorUrl] - Original post author's actor URL (for reply addressing)
|
||||||
* @param {string} [options.replyToActorHandle] - Original post author's handle (for Mention tag)
|
* @param {string} [options.replyToActorHandle] - Original post author's handle (for Mention tag)
|
||||||
* @returns {import("@fedify/fedify").Activity | null}
|
* @returns {Promise<import("@fedify/fedify").Activity | null>}
|
||||||
*/
|
*/
|
||||||
export function jf2ToAS2Activity(properties, actorUrl, publicationUrl, options = {}) {
|
export async function jf2ToAS2Activity(properties, actorUrl, publicationUrl, options = {}) {
|
||||||
const postType = properties["post-type"];
|
const postType = properties["post-type"];
|
||||||
const actorUri = new URL(actorUrl);
|
const actorUri = new URL(actorUrl);
|
||||||
|
|
||||||
// Likes are delivered as bookmarks — fall through to bookmark handling below
|
// Likes of ActivityPub objects are sent as a proper Like activity.
|
||||||
|
// Likes of regular URLs fall through to bookmark-style Create(Note) below.
|
||||||
|
if (postType === "like") {
|
||||||
|
const likeOfUrl = properties["like-of"];
|
||||||
|
if (likeOfUrl && (await isApUrl(likeOfUrl))) {
|
||||||
|
return new Like({
|
||||||
|
actor: actorUri,
|
||||||
|
object: new URL(likeOfUrl),
|
||||||
|
to: new URL("https://www.w3.org/ns/activitystreams#Public"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reposts are always public — upstream @rmdes addressing
|
// Reposts are always public — upstream @rmdes addressing
|
||||||
if (postType === "repost") {
|
if (postType === "repost") {
|
||||||
|
|||||||
Reference in New Issue
Block a user