diff --git a/assets/reader.css b/assets/reader.css index 958a247..7d0b8c5 100644 --- a/assets/reader.css +++ b/assets/reader.css @@ -976,34 +976,6 @@ gap: var(--space-m); } -.ap-compose__mode { - border: var(--border-width-thin) solid var(--color-outline); - border-radius: var(--border-radius-small); - display: flex; - flex-direction: column; - gap: var(--space-s); - padding: var(--space-m); -} - -.ap-compose__mode legend { - font-weight: 600; -} - -.ap-compose__mode-option { - cursor: pointer; - display: flex; - flex-wrap: wrap; - gap: var(--space-xs); -} - -.ap-compose__mode-hint { - color: var(--color-on-offset); - display: block; - font-size: var(--font-size-s); - margin-left: 1.5em; - width: 100%; -} - .ap-compose__editor { position: relative; } @@ -1027,21 +999,6 @@ outline-offset: -2px; } -.ap-compose__counter { - font-size: var(--font-size-s); - padding-top: var(--space-xs); - text-align: right; -} - -.ap-compose__counter--warn { - color: var(--color-yellow50); -} - -.ap-compose__counter--over { - color: var(--color-error); - font-weight: 600; -} - .ap-compose__syndication { border: var(--border-width-thin) solid var(--color-outline); border-radius: var(--border-radius-small); @@ -2837,11 +2794,6 @@ box-shadow: 0 2px 8px rgba(255, 255, 255, 0.06); } - /* --- Compose counter warning --- */ - .ap-compose__counter--warn { - color: var(--color-yellow90); - } - /* --- Tab badge federated: soften purple --- */ .ap-tab__badge--federated { color: var(--color-purple90); diff --git a/index.js b/index.js index 7f8fae5..4542da3 100644 --- a/index.js +++ b/index.js @@ -80,7 +80,6 @@ import { hashtagExploreApiController } from "./lib/controllers/hashtag-explore.j import { publicProfileController } from "./lib/controllers/public-profile.js"; import { authorizeInteractionController } from "./lib/controllers/authorize-interaction.js"; import { myProfileController } from "./lib/controllers/my-profile.js"; -import { noteObjectController } from "./lib/controllers/note-object.js"; import { refollowPauseController, refollowResumeController, @@ -189,10 +188,6 @@ 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)); - // Authorize interaction — remote follow / subscribe endpoint. // Remote servers redirect users here via the WebFinger subscribe template. router.get("/authorize_interaction", authorizeInteractionController(self)); @@ -889,7 +884,6 @@ export default class ActivityPubEndpoint { Indiekit.addCollection("ap_muted"); Indiekit.addCollection("ap_blocked"); Indiekit.addCollection("ap_interactions"); - Indiekit.addCollection("ap_notes"); Indiekit.addCollection("ap_followed_tags"); // Explore tab collections Indiekit.addCollection("ap_explore_tabs"); @@ -911,7 +905,6 @@ 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"), ap_followed_tags: indiekitCollections.get("ap_followed_tags"), // Explore tab collections ap_explore_tabs: indiekitCollections.get("ap_explore_tabs"), diff --git a/lib/controllers/compose.js b/lib/controllers/compose.js index 5f6d353..c916a60 100644 --- a/lib/controllers/compose.js +++ b/lib/controllers/compose.js @@ -1,11 +1,9 @@ /** - * Compose controllers — reply form via Micropub or direct AP. + * Compose controllers — reply form via Micropub. */ -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. @@ -155,7 +153,7 @@ export function composeController(mountPath, plugin) { } /** - * POST /admin/reader/compose — Submit reply via Micropub or direct AP. + * POST /admin/reader/compose — Submit reply via Micropub. * @param {string} mountPath - Plugin mount path * @param {object} plugin - ActivityPub plugin instance */ @@ -170,7 +168,7 @@ export function submitComposeController(mountPath, plugin) { } const { application } = request.app.locals; - const { content, mode } = request.body; + const { content } = request.body; const inReplyTo = request.body["in-reply-to"]; const syndicateTo = request.body["mp-syndicate-to"]; @@ -181,122 +179,7 @@ export function submitComposeController(mountPath, plugin) { }); } - // Quick reply — direct AP - if (mode === "quick") { - if (!plugin._federation) { - return response.status(503).render("error", { - title: "Error", - content: "Federation not initialized", - }); - } - - const { Create, Note } = await import("@fedify/fedify/vocab"); - const handle = plugin.options.actor.handle; - const ctx = plugin._federation.createContext( - new URL(plugin._publicationUrl), - { handle, publicationUrl: plugin._publicationUrl }, - ); - - const uuid = crypto.randomUUID(); - const baseUrl = plugin._publicationUrl.replace(/\/$/, ""); - const noteId = `${baseUrl}/activitypub/quick-replies/${uuid}`; - const actorUri = ctx.getActorUri(handle); - - const publicAddress = new URL( - "https://www.w3.org/ns/activitystreams#Public", - ); - 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) { - recipient = await resolveAuthor( - inReplyTo, - ctx, - documentLoader, - application?.collections, - ); - } - - // Build cc list: always include followers, add original author for replies - const ccList = [followersUri]; - if (recipient?.id) { - ccList.push(recipient.id); - } - - const note = new Note({ - id: new URL(noteId), - attribution: actorUri, - content: content.trim(), - replyTarget: inReplyTo ? new URL(inReplyTo) : undefined, - published: Temporal.Now.instant(), - to: publicAddress, - ccs: ccList, - }); - - const create = new Create({ - id: new URL(`${noteId}#activity`), - actor: actorUri, - object: note, - to: publicAddress, - 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, - syncCollection: true, - orderingKey: noteId, - }); - - // Also send directly to the original author's inbox - if (recipient) { - try { - await ctx.sendActivity( - { identifier: handle }, - recipient, - create, - { orderingKey: noteId }, - ); - console.info( - `[ActivityPub] Sent quick reply directly to ${recipient.id?.href || "author"}`, - ); - } catch (error) { - console.warn( - `[ActivityPub] Direct delivery to author failed (quick reply):`, - error.message, - ); - } - } - - console.info( - `[ActivityPub] Sent quick reply${inReplyTo ? ` to ${inReplyTo}` : ""}`, - ); - - return response.redirect(`${mountPath}/admin/reader`); - } - - // Micropub path — post as blog reply + // Post as blog reply via Micropub const micropubEndpoint = application.micropubEndpoint; if (!micropubEndpoint) { diff --git a/lib/controllers/note-object.js b/lib/controllers/note-object.js deleted file mode 100644 index 8e6dffa..0000000 --- a/lib/controllers/note-object.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 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); - }; -} diff --git a/locales/en.json b/locales/en.json index 492947d..16b54dd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -141,15 +141,9 @@ }, "compose": { "title": "Compose reply", - "modeLabel": "Reply mode", - "modeMicropub": "Post as blog reply", - "modeMicropubHint": "Creates a permanent post on your blog, syndicated to the fediverse", - "modeQuick": "Quick reply", - "modeQuickHint": "Sends a reply directly to the fediverse (no blog post created)", "placeholder": "Write your reply…", "syndicateLabel": "Syndicate to", "submitMicropub": "Post reply", - "submitQuick": "Send reply", "cancel": "Cancel", "errorEmpty": "Reply content cannot be empty" }, diff --git a/package.json b/package.json index a1c65c0..b6e2c34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "2.6.2", + "version": "2.7.0", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "keywords": [ "indiekit", diff --git a/views/activitypub-compose.njk b/views/activitypub-compose.njk index 3289931..c1783fb 100644 --- a/views/activitypub-compose.njk +++ b/views/activitypub-compose.njk @@ -21,50 +21,23 @@ {% endif %} -
+ {% if replyTo %} {% endif %} - {# Mode toggle #} -
- {{ __("activitypub.compose.modeLabel") }} - - -
- {# Content textarea #}
-
- -
- {# Syndication targets (Micropub mode only) #} + {# Syndication targets #} {% if syndicationTargets.length > 0 %} -
+