mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
feat: remove quick reply, streamline blog reply (v2.7.0)
Remove the quick-reply code path entirely — all replies now go through Micropub as blog posts. Quick replies created orphan URLs that served raw JSON-LD to browsers and caused unreadable links in conversations. - Delete quick-reply controller (note-object.js) and route - Remove ap_notes collection registration - Simplify compose form: no mode toggle, no character counter - Remove quick-reply CSS and locale strings Confab-Link: http://localhost:8080/sessions/d116ad5b-ef8a-424e-9ebe-76c06bef1df6
This commit is contained in:
@@ -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);
|
||||
|
||||
7
index.js
7
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"),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -21,50 +21,23 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ mountPath }}/admin/reader/compose" class="ap-compose__form"
|
||||
x-data="{
|
||||
mode: 'micropub',
|
||||
content: '',
|
||||
maxChars: 500,
|
||||
get remaining() { return this.maxChars - this.content.length; }
|
||||
}">
|
||||
<form method="post" action="{{ mountPath }}/admin/reader/compose" class="ap-compose__form">
|
||||
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
|
||||
{% if replyTo %}
|
||||
<input type="hidden" name="in-reply-to" value="{{ replyTo }}">
|
||||
{% endif %}
|
||||
|
||||
{# Mode toggle #}
|
||||
<fieldset class="ap-compose__mode">
|
||||
<legend>{{ __("activitypub.compose.modeLabel") }}</legend>
|
||||
<label class="ap-compose__mode-option">
|
||||
<input type="radio" name="mode" value="micropub" x-model="mode" checked>
|
||||
{{ __("activitypub.compose.modeMicropub") }}
|
||||
<span class="ap-compose__mode-hint">{{ __("activitypub.compose.modeMicropubHint") }}</span>
|
||||
</label>
|
||||
<label class="ap-compose__mode-option">
|
||||
<input type="radio" name="mode" value="quick" x-model="mode">
|
||||
{{ __("activitypub.compose.modeQuick") }}
|
||||
<span class="ap-compose__mode-hint">{{ __("activitypub.compose.modeQuickHint") }}</span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
{# Content textarea #}
|
||||
<div class="ap-compose__editor">
|
||||
<textarea name="content" class="ap-compose__textarea"
|
||||
rows="6"
|
||||
:maxlength="mode === 'quick' ? maxChars : undefined"
|
||||
x-model="content"
|
||||
placeholder="{{ __('activitypub.compose.placeholder') }}"
|
||||
required></textarea>
|
||||
<div class="ap-compose__counter" x-show="mode === 'quick'" x-cloak>
|
||||
<span :class="{ 'ap-compose__counter--warn': remaining < 50, 'ap-compose__counter--over': remaining < 0 }"
|
||||
x-text="remaining"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Syndication targets (Micropub mode only) #}
|
||||
{# Syndication targets #}
|
||||
{% if syndicationTargets.length > 0 %}
|
||||
<fieldset class="ap-compose__syndication" x-show="mode === 'micropub'">
|
||||
<fieldset class="ap-compose__syndication">
|
||||
<legend>{{ __("activitypub.compose.syndicateLabel") }}</legend>
|
||||
{% for target in syndicationTargets %}
|
||||
<label class="ap-compose__syndication-target">
|
||||
@@ -77,8 +50,7 @@
|
||||
|
||||
<div class="ap-compose__actions">
|
||||
<button type="submit" class="ap-compose__submit">
|
||||
<span x-show="mode === 'micropub'">{{ __("activitypub.compose.submitMicropub") }}</span>
|
||||
<span x-show="mode === 'quick'">{{ __("activitypub.compose.submitQuick") }}</span>
|
||||
{{ __("activitypub.compose.submitMicropub") }}
|
||||
</button>
|
||||
<a href="{{ mountPath }}/admin/reader" class="ap-compose__cancel">
|
||||
{{ __("activitypub.compose.cancel") }}
|
||||
|
||||
Reference in New Issue
Block a user