mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
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:
2
index.js
2
index.js
@@ -583,7 +583,7 @@ export default class ActivityPubEndpoint {
|
||||
}
|
||||
}
|
||||
|
||||
const activity = jf2ToAS2Activity(
|
||||
const activity = await jf2ToAS2Activity(
|
||||
properties,
|
||||
actorUrl,
|
||||
self._publicationUrl,
|
||||
|
||||
@@ -560,7 +560,7 @@ function setupFeatured(federation, mountPath, handle, collections, publicationUr
|
||||
});
|
||||
if (!post) continue;
|
||||
const actorUrl = ctx.getActorUri(identifier).href;
|
||||
const activity = jf2ToAS2Activity(
|
||||
const activity = await jf2ToAS2Activity(
|
||||
post.properties,
|
||||
actorUrl,
|
||||
publicationUrl,
|
||||
@@ -637,19 +637,21 @@ function setupOutbox(federation, mountPath, handle, collections) {
|
||||
.toArray();
|
||||
|
||||
const { jf2ToAS2Activity } = await import("./jf2-to-as2.js");
|
||||
const items = posts
|
||||
.map((post) => {
|
||||
try {
|
||||
return jf2ToAS2Activity(
|
||||
post.properties,
|
||||
ctx.getActorUri(identifier).href,
|
||||
collections._publicationUrl,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
const items = (
|
||||
await Promise.all(
|
||||
posts.map(async (post) => {
|
||||
try {
|
||||
return await jf2ToAS2Activity(
|
||||
post.properties,
|
||||
ctx.getActorUri(identifier).href,
|
||||
collections._publicationUrl,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter(Boolean);
|
||||
|
||||
return {
|
||||
items,
|
||||
@@ -687,7 +689,7 @@ function setupObjectDispatchers(federation, mountPath, handle, collections, publ
|
||||
// Soft-deleted posts should not be dereferenceable
|
||||
if (post.properties?.deleted) return null;
|
||||
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
|
||||
if (!(activity instanceof Create)) return null;
|
||||
return await activity.getObject();
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Create,
|
||||
Hashtag,
|
||||
Image,
|
||||
Like,
|
||||
Mention,
|
||||
Note,
|
||||
Video,
|
||||
@@ -79,6 +80,30 @@ function linkifyMentions(html, resolvedMentions) {
|
||||
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)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -239,13 +264,24 @@ export function jf2ToActivityStreams(properties, actorUrl, publicationUrl, optio
|
||||
* @param {object} [options] - Optional settings
|
||||
* @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)
|
||||
* @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 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
|
||||
if (postType === "repost") {
|
||||
|
||||
Reference in New Issue
Block a user