mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
fix: store and serve quick reply Notes for remote dereferencing
Remote servers (Mastodon, Bonfire) dereference Note IDs to verify Create activities. Quick reply Notes had no public route — servers got 302 to login and rejected the activity. - Store quick reply Note data in ap_notes collection - Add public GET /quick-replies/:id serving JSON-LD - Use shared resolveAuthor() in compose.js for quick replies
This commit is contained in:
7
index.js
7
index.js
@@ -59,6 +59,7 @@ import {
|
||||
} from "./lib/controllers/featured-tags.js";
|
||||
import { resolveController } from "./lib/controllers/resolve.js";
|
||||
import { publicProfileController } from "./lib/controllers/public-profile.js";
|
||||
import { noteObjectController } from "./lib/controllers/note-object.js";
|
||||
import {
|
||||
refollowPauseController,
|
||||
refollowResumeController,
|
||||
@@ -161,6 +162,10 @@ export default class ActivityPubEndpoint {
|
||||
return self._fedifyMiddleware(req, res, next);
|
||||
});
|
||||
|
||||
// Serve stored quick reply Notes as JSON-LD so remote servers can
|
||||
// dereference the Note ID during Create activity verification.
|
||||
router.get("/quick-replies/:id", noteObjectController(self));
|
||||
|
||||
// HTML fallback for actor URL — serve a public profile page.
|
||||
// Fedify only serves JSON-LD; browsers get 406 and fall through here.
|
||||
router.get("/users/:identifier", publicProfileController(self));
|
||||
@@ -835,6 +840,7 @@ export default class ActivityPubEndpoint {
|
||||
Indiekit.addCollection("ap_muted");
|
||||
Indiekit.addCollection("ap_blocked");
|
||||
Indiekit.addCollection("ap_interactions");
|
||||
Indiekit.addCollection("ap_notes");
|
||||
|
||||
// Store collection references (posts resolved lazily)
|
||||
const indiekitCollections = Indiekit.collections;
|
||||
@@ -853,6 +859,7 @@ export default class ActivityPubEndpoint {
|
||||
ap_muted: indiekitCollections.get("ap_muted"),
|
||||
ap_blocked: indiekitCollections.get("ap_blocked"),
|
||||
ap_interactions: indiekitCollections.get("ap_interactions"),
|
||||
ap_notes: indiekitCollections.get("ap_notes"),
|
||||
get posts() {
|
||||
return indiekitCollections.get("posts");
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { Temporal } from "@js-temporal/polyfill";
|
||||
import { getToken, validateToken } from "../csrf.js";
|
||||
import { sanitizeContent } from "../timeline-store.js";
|
||||
import { resolveAuthor } from "../resolve-author.js";
|
||||
|
||||
/**
|
||||
* Fetch syndication targets from the Micropub config endpoint.
|
||||
@@ -205,33 +206,20 @@ export function submitComposeController(mountPath, plugin) {
|
||||
);
|
||||
const followersUri = ctx.getFollowersUri(handle);
|
||||
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
|
||||
// Resolve the original author BEFORE constructing the Note,
|
||||
// so we can include them in cc (required for threading/notification)
|
||||
let recipient = null;
|
||||
if (inReplyTo) {
|
||||
try {
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
const remoteObject = await ctx.lookupObject(new URL(inReplyTo), {
|
||||
documentLoader,
|
||||
});
|
||||
|
||||
if (
|
||||
remoteObject &&
|
||||
typeof remoteObject.getAttributedTo === "function"
|
||||
) {
|
||||
const author = await remoteObject.getAttributedTo({
|
||||
documentLoader,
|
||||
});
|
||||
recipient = Array.isArray(author) ? author[0] : author;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[ActivityPub] lookupObject failed for ${inReplyTo} (quick reply):`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
recipient = await resolveAuthor(
|
||||
inReplyTo,
|
||||
ctx,
|
||||
documentLoader,
|
||||
application?.collections,
|
||||
);
|
||||
}
|
||||
|
||||
// Build cc list: always include followers, add original author for replies
|
||||
@@ -258,6 +246,21 @@ export function submitComposeController(mountPath, plugin) {
|
||||
ccs: ccList,
|
||||
});
|
||||
|
||||
// Store the Note so remote servers can dereference its ID
|
||||
const ap_notes = application?.collections?.get("ap_notes");
|
||||
if (ap_notes) {
|
||||
await ap_notes.insertOne({
|
||||
_id: uuid,
|
||||
noteId,
|
||||
actorUrl: actorUri.href,
|
||||
content: content.trim(),
|
||||
inReplyTo: inReplyTo || null,
|
||||
published: new Date().toISOString(),
|
||||
to: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
cc: ccList.map((u) => (u instanceof URL ? u.href : u.href || u)),
|
||||
});
|
||||
}
|
||||
|
||||
// Send to followers
|
||||
await ctx.sendActivity({ identifier: handle }, "followers", create, {
|
||||
preferSharedInbox: true,
|
||||
|
||||
51
lib/controllers/note-object.js
Normal file
51
lib/controllers/note-object.js
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Public route handler for serving quick reply Notes as ActivityPub JSON-LD.
|
||||
*
|
||||
* Remote servers dereference Note IDs to verify Create activities.
|
||||
* Without this, quick replies are rejected by servers that validate
|
||||
* the Note's ID URL (Mastodon with Authorized Fetch, Bonfire, etc.).
|
||||
*/
|
||||
|
||||
/**
|
||||
* GET /quick-replies/:id — serve a stored Note as JSON-LD.
|
||||
* @param {object} plugin - ActivityPub plugin instance
|
||||
*/
|
||||
export function noteObjectController(plugin) {
|
||||
return async (request, response) => {
|
||||
const { id } = request.params;
|
||||
|
||||
const { application } = request.app.locals;
|
||||
const ap_notes = application?.collections?.get("ap_notes");
|
||||
|
||||
if (!ap_notes) {
|
||||
return response.status(404).json({ error: "Not Found" });
|
||||
}
|
||||
|
||||
const note = await ap_notes.findOne({ _id: id });
|
||||
|
||||
if (!note) {
|
||||
return response.status(404).json({ error: "Not Found" });
|
||||
}
|
||||
|
||||
const noteJson = {
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
id: note.noteId,
|
||||
type: "Note",
|
||||
attributedTo: note.actorUrl,
|
||||
content: note.content,
|
||||
published: note.published,
|
||||
to: note.to,
|
||||
cc: note.cc,
|
||||
};
|
||||
|
||||
if (note.inReplyTo) {
|
||||
noteJson.inReplyTo = note.inReplyTo;
|
||||
}
|
||||
|
||||
response
|
||||
.status(200)
|
||||
.set("Content-Type", "application/activity+json; charset=utf-8")
|
||||
.set("Cache-Control", "public, max-age=3600")
|
||||
.json(noteJson);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user