mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
fix: reader UI fixes and correct Fedify API usage (v1.1.8→1.1.12)
- Fix Unknown authors by adding multi-strategy fallback chain in extractObjectData (getAttributedTo → actorFallback → attributionIds) - Fix empty boosts from Lemmy/PieFed by checking content before storing - Fix @mention/hashtag styling to stay inline instead of breaking layout - Fix compose reply to show sanitized HTML blockquote instead of raw text - Add default-checked syndication targets for AP and Bluesky - Use authenticated document loader for all lookupObject calls (fixes 401 errors on servers requiring Authorized Fetch) - Fix like handler 404 by using canonical AP uid for interactions instead of display URLs; add data-item-uid to card template - Fix profile bio showing Nunjucks macro source code by renaming summary→bio to avoid collision with Indiekit's summary macro - Fix Fedify API misuse in timeline-store.js: use instanceof Article (not string comparison), replyTargetId (not inReplyTo), getTags() and getAttachments() async methods (not sync property access) - Fix inbox-listeners.js: use replyTargetId instead of non-existent getInReplyTo(), use instanceof Article for Update handler - Add error logging to interaction catch blocks
This commit is contained in:
@@ -296,24 +296,40 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* @mentions — styled as subtle pills to distinguish from prose */
|
||||
.ap-card__content .h-card,
|
||||
.ap-card__content a.u-url.mention {
|
||||
color: var(--color-on-offset);
|
||||
font-size: var(--font-size-s);
|
||||
text-decoration: none;
|
||||
/* @mentions — keep inline, style as subtle links */
|
||||
.ap-card__content .h-card {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.ap-card__content .h-card a,
|
||||
.ap-card__content a.u-url.mention {
|
||||
display: inline;
|
||||
color: var(--color-on-offset);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ap-card__content .h-card a span,
|
||||
.ap-card__content a.u-url.mention span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.ap-card__content .h-card a:hover,
|
||||
.ap-card__content a.u-url.mention:hover {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Hashtag mentions — subtle tag styling */
|
||||
/* Hashtag mentions — keep inline, subtle styling */
|
||||
.ap-card__content a.mention.hashtag {
|
||||
display: inline;
|
||||
color: var(--color-on-offset);
|
||||
font-size: var(--font-size-s);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ap-card__content a.mention.hashtag span {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.ap-card__content a.mention.hashtag:hover {
|
||||
|
||||
16
index.js
16
index.js
@@ -496,7 +496,13 @@ export default class ActivityPubEndpoint {
|
||||
);
|
||||
|
||||
// Resolve the remote actor to get their inbox
|
||||
const remoteActor = await ctx.lookupObject(actorUrl);
|
||||
// Use authenticated document loader for servers requiring Authorized Fetch
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
const remoteActor = await ctx.lookupObject(actorUrl, {
|
||||
documentLoader,
|
||||
});
|
||||
if (!remoteActor) {
|
||||
return { ok: false, error: "Could not resolve remote actor" };
|
||||
}
|
||||
@@ -591,7 +597,13 @@ export default class ActivityPubEndpoint {
|
||||
{ handle, publicationUrl: this._publicationUrl },
|
||||
);
|
||||
|
||||
const remoteActor = await ctx.lookupObject(actorUrl);
|
||||
// Use authenticated document loader for servers requiring Authorized Fetch
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
const remoteActor = await ctx.lookupObject(actorUrl, {
|
||||
documentLoader,
|
||||
});
|
||||
if (!remoteActor) {
|
||||
// Even if we can't resolve, remove locally
|
||||
await this._collections.ap_following.deleteOne({ actorUrl });
|
||||
|
||||
@@ -227,8 +227,13 @@ async function processOneFollow(options, entry) {
|
||||
try {
|
||||
const ctx = federation.createContext(new URL(publicationUrl), { handle, publicationUrl });
|
||||
|
||||
// Resolve the remote actor
|
||||
const remoteActor = await ctx.lookupObject(entry.actorUrl);
|
||||
// Resolve the remote actor (signed request for Authorized Fetch)
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
const remoteActor = await ctx.lookupObject(entry.actorUrl, {
|
||||
documentLoader,
|
||||
});
|
||||
if (!remoteActor) {
|
||||
throw new Error("Could not resolve remote actor");
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
*/
|
||||
|
||||
import { Temporal } from "@js-temporal/polyfill";
|
||||
import { getTimelineItem } from "../storage/timeline.js";
|
||||
import { getToken, validateToken } from "../csrf.js";
|
||||
import { sanitizeContent } from "../timeline-store.js";
|
||||
|
||||
/**
|
||||
* Fetch syndication targets from the Micropub config endpoint.
|
||||
@@ -61,7 +61,12 @@ export function composeController(mountPath, plugin) {
|
||||
};
|
||||
|
||||
// Try to find the post in our timeline first
|
||||
replyContext = await getTimelineItem(collections, replyTo);
|
||||
// Note: Timeline stores uid (canonical AP URL) and url (display URL).
|
||||
// The card link passes the display URL, so search both fields.
|
||||
const ap_timeline = collections.ap_timeline;
|
||||
replyContext = ap_timeline
|
||||
? await ap_timeline.findOne({ $or: [{ uid: replyTo }, { url: replyTo }] })
|
||||
: null;
|
||||
|
||||
// If not in timeline, try to look up remotely
|
||||
if (!replyContext && plugin._federation) {
|
||||
@@ -71,14 +76,22 @@ export function composeController(mountPath, plugin) {
|
||||
new URL(plugin._publicationUrl),
|
||||
{ handle, publicationUrl: plugin._publicationUrl },
|
||||
);
|
||||
const remoteObject = await ctx.lookupObject(new URL(replyTo));
|
||||
// Use authenticated document loader for Authorized Fetch
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
const remoteObject = await ctx.lookupObject(new URL(replyTo), {
|
||||
documentLoader,
|
||||
});
|
||||
|
||||
if (remoteObject) {
|
||||
let authorName = "";
|
||||
let authorUrl = "";
|
||||
|
||||
if (typeof remoteObject.getAttributedTo === "function") {
|
||||
const author = await remoteObject.getAttributedTo();
|
||||
const author = await remoteObject.getAttributedTo({
|
||||
documentLoader,
|
||||
});
|
||||
const actor = Array.isArray(author) ? author[0] : author;
|
||||
|
||||
if (actor) {
|
||||
@@ -90,18 +103,22 @@ export function composeController(mountPath, plugin) {
|
||||
}
|
||||
}
|
||||
|
||||
const rawHtml = remoteObject.content?.toString() || "";
|
||||
replyContext = {
|
||||
url: replyTo,
|
||||
name: remoteObject.name?.toString() || "",
|
||||
content: {
|
||||
text:
|
||||
remoteObject.content?.toString()?.slice(0, 300) || "",
|
||||
html: sanitizeContent(rawHtml),
|
||||
text: rawHtml.replace(/<[^>]*>/g, "").slice(0, 300),
|
||||
},
|
||||
author: { name: authorName, url: authorUrl },
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Could not resolve — form still works without context
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[ActivityPub] lookupObject failed for ${replyTo} (compose):`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +129,13 @@ export function composeController(mountPath, plugin) {
|
||||
? await getSyndicationTargets(application, token)
|
||||
: [];
|
||||
|
||||
// Default-check only AP (Fedify) and Bluesky targets
|
||||
// "@rick@rmendes.net" = AP Fedify, "@rmendes.net" = Bluesky
|
||||
for (const target of syndicationTargets) {
|
||||
const name = target.name || "";
|
||||
target.defaultChecked = name === "@rick@rmendes.net" || name === "@rmendes.net";
|
||||
}
|
||||
|
||||
const csrfToken = getToken(request.session);
|
||||
|
||||
response.render("activitypub-compose", {
|
||||
@@ -198,13 +222,20 @@ export function submitComposeController(mountPath, plugin) {
|
||||
// If replying, also send to the original author
|
||||
if (inReplyTo) {
|
||||
try {
|
||||
const remoteObject = await ctx.lookupObject(new URL(inReplyTo));
|
||||
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();
|
||||
const author = await remoteObject.getAttributedTo({
|
||||
documentLoader,
|
||||
});
|
||||
const recipient = Array.isArray(author)
|
||||
? author[0]
|
||||
: author;
|
||||
|
||||
@@ -57,15 +57,20 @@ export function boostController(mountPath, plugin) {
|
||||
orderingKey: url,
|
||||
});
|
||||
|
||||
// Also send to the original post author
|
||||
// Also send to the original post author (signed request for Authorized Fetch)
|
||||
try {
|
||||
const remoteObject = await ctx.lookupObject(new URL(url));
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
const remoteObject = await ctx.lookupObject(new URL(url), {
|
||||
documentLoader,
|
||||
});
|
||||
|
||||
if (
|
||||
remoteObject &&
|
||||
typeof remoteObject.getAttributedTo === "function"
|
||||
) {
|
||||
const author = await remoteObject.getAttributedTo();
|
||||
const author = await remoteObject.getAttributedTo({ documentLoader });
|
||||
const recipient = Array.isArray(author) ? author[0] : author;
|
||||
|
||||
if (recipient) {
|
||||
@@ -77,8 +82,11 @@ export function boostController(mountPath, plugin) {
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-critical — followers still received the boost
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[ActivityPub] lookupObject failed for ${url} (boost):`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
|
||||
// Track the interaction
|
||||
|
||||
@@ -43,29 +43,57 @@ export function likeController(mountPath, plugin) {
|
||||
{ handle, publicationUrl: plugin._publicationUrl },
|
||||
);
|
||||
|
||||
// Look up the remote post to find its author
|
||||
const remoteObject = await ctx.lookupObject(new URL(url));
|
||||
// Use authenticated document loader for servers requiring Authorized Fetch
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
|
||||
if (!remoteObject) {
|
||||
return response.status(404).json({
|
||||
success: false,
|
||||
error: "Could not resolve remote post",
|
||||
});
|
||||
}
|
||||
|
||||
// Get the post author for delivery
|
||||
// Resolve author for delivery — try multiple strategies
|
||||
let recipient = null;
|
||||
|
||||
if (typeof remoteObject.getAttributedTo === "function") {
|
||||
const author = await remoteObject.getAttributedTo();
|
||||
recipient = Array.isArray(author) ? author[0] : author;
|
||||
// Strategy 1: Look up remote post via Fedify (signed request)
|
||||
try {
|
||||
const remoteObject = await ctx.lookupObject(new URL(url), {
|
||||
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 ${url}:`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
|
||||
// Strategy 2: Use author URL from our timeline (already stored)
|
||||
// Note: Timeline items store both uid (canonical AP URL) and url (display URL).
|
||||
// The card passes the display URL, so we search by both fields.
|
||||
if (!recipient) {
|
||||
return response.status(404).json({
|
||||
success: false,
|
||||
error: "Could not resolve post author",
|
||||
});
|
||||
const { application } = request.app.locals;
|
||||
const ap_timeline = application?.collections?.get("ap_timeline");
|
||||
const timelineItem = ap_timeline
|
||||
? await ap_timeline.findOne({ $or: [{ uid: url }, { url }] })
|
||||
: null;
|
||||
const authorUrl = timelineItem?.author?.url;
|
||||
|
||||
if (authorUrl) {
|
||||
try {
|
||||
recipient = await ctx.lookupObject(new URL(authorUrl), {
|
||||
documentLoader,
|
||||
});
|
||||
} catch {
|
||||
// Could not resolve author actor either
|
||||
}
|
||||
}
|
||||
|
||||
if (!recipient) {
|
||||
return response.status(404).json({
|
||||
success: false,
|
||||
error: "Could not resolve post author",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a unique activity ID
|
||||
@@ -170,13 +198,45 @@ export function unlikeController(mountPath, plugin) {
|
||||
{ handle, publicationUrl: plugin._publicationUrl },
|
||||
);
|
||||
|
||||
// Resolve the recipient
|
||||
const remoteObject = await ctx.lookupObject(new URL(url));
|
||||
// Use authenticated document loader for servers requiring Authorized Fetch
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
|
||||
// Resolve the recipient — try remote first, then timeline fallback
|
||||
let recipient = null;
|
||||
|
||||
if (remoteObject && typeof remoteObject.getAttributedTo === "function") {
|
||||
const author = await remoteObject.getAttributedTo();
|
||||
recipient = Array.isArray(author) ? author[0] : author;
|
||||
try {
|
||||
const remoteObject = await ctx.lookupObject(new URL(url), {
|
||||
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 ${url} (unlike):`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
|
||||
if (!recipient) {
|
||||
const ap_timeline = application?.collections?.get("ap_timeline");
|
||||
const timelineItem = ap_timeline
|
||||
? await ap_timeline.findOne({ $or: [{ uid: url }, { url }] })
|
||||
: null;
|
||||
const authorUrl = timelineItem?.author?.url;
|
||||
|
||||
if (authorUrl) {
|
||||
try {
|
||||
recipient = await ctx.lookupObject(new URL(authorUrl), {
|
||||
documentLoader,
|
||||
});
|
||||
} catch {
|
||||
// Could not resolve — will proceed to cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!recipient) {
|
||||
|
||||
@@ -151,7 +151,12 @@ export function blockController(mountPath, plugin) {
|
||||
{ handle, publicationUrl: plugin._publicationUrl },
|
||||
);
|
||||
|
||||
const remoteActor = await ctx.lookupObject(new URL(url));
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
const remoteActor = await ctx.lookupObject(new URL(url), {
|
||||
documentLoader,
|
||||
});
|
||||
|
||||
if (remoteActor) {
|
||||
const block = new Block({
|
||||
@@ -225,7 +230,12 @@ export function unblockController(mountPath, plugin) {
|
||||
{ handle, publicationUrl: plugin._publicationUrl },
|
||||
);
|
||||
|
||||
const remoteActor = await ctx.lookupObject(new URL(url));
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
const remoteActor = await ctx.lookupObject(new URL(url), {
|
||||
documentLoader,
|
||||
});
|
||||
|
||||
if (remoteActor) {
|
||||
const block = new Block({
|
||||
|
||||
@@ -36,11 +36,14 @@ export function remoteProfileController(mountPath, plugin) {
|
||||
{ handle, publicationUrl: plugin._publicationUrl },
|
||||
);
|
||||
|
||||
// Look up the remote actor
|
||||
// Look up the remote actor (signed request for Authorized Fetch)
|
||||
const documentLoader = await ctx.getDocumentLoader({
|
||||
identifier: handle,
|
||||
});
|
||||
let actor;
|
||||
|
||||
try {
|
||||
actor = await ctx.lookupObject(new URL(actorUrl));
|
||||
actor = await ctx.lookupObject(new URL(actorUrl), { documentLoader });
|
||||
} catch {
|
||||
return response.status(404).render("error", {
|
||||
title: "Error",
|
||||
@@ -61,7 +64,7 @@ export function remoteProfileController(mountPath, plugin) {
|
||||
actor.preferredUsername?.toString() ||
|
||||
actorUrl;
|
||||
const actorHandle = actor.preferredUsername?.toString() || "";
|
||||
const summary = sanitizeContent(actor.summary?.toString() || "");
|
||||
const bio = sanitizeContent(actor.summary?.toString() || "");
|
||||
let icon = "";
|
||||
let image = "";
|
||||
|
||||
@@ -126,7 +129,7 @@ export function remoteProfileController(mountPath, plugin) {
|
||||
actorUrl,
|
||||
name,
|
||||
actorHandle,
|
||||
summary,
|
||||
bio,
|
||||
icon,
|
||||
image,
|
||||
instanceHost,
|
||||
|
||||
@@ -107,26 +107,47 @@ export function readerController(mountPath) {
|
||||
const unreadCount = await getUnreadNotificationCount(collections);
|
||||
|
||||
// Get interaction state for liked/boosted indicators
|
||||
// Interactions are keyed by canonical AP uid (new) or display url (legacy).
|
||||
// Query by both, normalize map keys to uid for template lookup.
|
||||
const interactionsCol =
|
||||
application?.collections?.get("ap_interactions");
|
||||
const interactionMap = {};
|
||||
|
||||
if (interactionsCol) {
|
||||
const itemUrls = items
|
||||
.map((item) => item.url || item.originalUrl)
|
||||
.filter(Boolean);
|
||||
const lookupUrls = new Set();
|
||||
const objectUrlToUid = new Map();
|
||||
|
||||
if (itemUrls.length > 0) {
|
||||
for (const item of items) {
|
||||
const uid = item.uid;
|
||||
const displayUrl = item.url || item.originalUrl;
|
||||
|
||||
if (uid) {
|
||||
lookupUrls.add(uid);
|
||||
objectUrlToUid.set(uid, uid);
|
||||
}
|
||||
|
||||
if (displayUrl) {
|
||||
lookupUrls.add(displayUrl);
|
||||
objectUrlToUid.set(displayUrl, uid || displayUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (lookupUrls.size > 0) {
|
||||
const interactions = await interactionsCol
|
||||
.find({ objectUrl: { $in: itemUrls } })
|
||||
.find({ objectUrl: { $in: [...lookupUrls] } })
|
||||
.toArray();
|
||||
|
||||
for (const interaction of interactions) {
|
||||
if (!interactionMap[interaction.objectUrl]) {
|
||||
interactionMap[interaction.objectUrl] = {};
|
||||
// Normalize to uid so template can look up by itemUid
|
||||
const key =
|
||||
objectUrlToUid.get(interaction.objectUrl) ||
|
||||
interaction.objectUrl;
|
||||
|
||||
if (!interactionMap[key]) {
|
||||
interactionMap[key] = {};
|
||||
}
|
||||
|
||||
interactionMap[interaction.objectUrl][interaction.type] = true;
|
||||
interactionMap[key][interaction.type] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Accept,
|
||||
Add,
|
||||
Announce,
|
||||
Article,
|
||||
Block,
|
||||
Create,
|
||||
Delete,
|
||||
@@ -365,14 +366,8 @@ export function registerInboxListeners(inboxChain, options) {
|
||||
actorObj?.preferredUsername?.toString() ||
|
||||
actorUrl;
|
||||
|
||||
let inReplyTo = null;
|
||||
if (object instanceof Note && typeof object.getInReplyTo === "function") {
|
||||
try {
|
||||
inReplyTo = (await object.getInReplyTo())?.id?.href ?? null;
|
||||
} catch {
|
||||
/* remote fetch may fail */
|
||||
}
|
||||
}
|
||||
// Use replyTargetId (non-fetching) for the inReplyTo URL
|
||||
const inReplyTo = object.replyTargetId?.href || null;
|
||||
|
||||
// Log replies to our posts (existing behavior for conversations)
|
||||
const pubUrl = collections._publicationUrl;
|
||||
@@ -505,7 +500,7 @@ export function registerInboxListeners(inboxChain, options) {
|
||||
}
|
||||
|
||||
// PATH 1: If object is a Note/Article → Update timeline item content
|
||||
if (object && (object instanceof Note || object.type === "Article")) {
|
||||
if (object && (object instanceof Note || object instanceof Article)) {
|
||||
const objectUrl = object.id?.href || "";
|
||||
if (objectUrl) {
|
||||
try {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* @module timeline-store
|
||||
*/
|
||||
|
||||
import { Article } from "@fedify/fedify";
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
/**
|
||||
@@ -98,9 +99,9 @@ export async function extractObjectData(object, options = {}) {
|
||||
const uid = object.id?.href || "";
|
||||
const url = object.url?.href || uid;
|
||||
|
||||
// Determine type
|
||||
// Determine type — use instanceof for Fedify vocab objects
|
||||
let type = "note";
|
||||
if (object.type?.toLowerCase() === "article") {
|
||||
if (object instanceof Article) {
|
||||
type = "article";
|
||||
}
|
||||
if (options.boostedBy) {
|
||||
@@ -179,42 +180,51 @@ export async function extractObjectData(object, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract tags/categories
|
||||
// Extract tags/categories — Fedify uses async getTags()
|
||||
const category = [];
|
||||
if (object.tag) {
|
||||
const tags = Array.isArray(object.tag) ? object.tag : [object.tag];
|
||||
for (const tag of tags) {
|
||||
if (tag.type === "Hashtag" && tag.name) {
|
||||
category.push(tag.name.toString().replace(/^#/, ""));
|
||||
try {
|
||||
if (typeof object.getTags === "function") {
|
||||
const tags = await object.getTags();
|
||||
for (const tag of tags) {
|
||||
if (tag.name) {
|
||||
const tagName = tag.name.toString().replace(/^#/, "");
|
||||
if (tagName) category.push(tagName);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Tags extraction failed — non-critical
|
||||
}
|
||||
|
||||
// Extract media attachments
|
||||
// Extract media attachments — Fedify uses async getAttachments()
|
||||
const photo = [];
|
||||
const video = [];
|
||||
const audio = [];
|
||||
|
||||
if (object.attachment) {
|
||||
const attachments = Array.isArray(object.attachment) ? object.attachment : [object.attachment];
|
||||
for (const att of attachments) {
|
||||
const mediaUrl = att.url?.href || "";
|
||||
if (!mediaUrl) continue;
|
||||
try {
|
||||
if (typeof object.getAttachments === "function") {
|
||||
const attachments = await object.getAttachments();
|
||||
for (const att of attachments) {
|
||||
const mediaUrl = att.url?.href || "";
|
||||
if (!mediaUrl) continue;
|
||||
|
||||
const mediaType = att.mediaType?.toLowerCase() || "";
|
||||
const mediaType = att.mediaType?.toLowerCase() || "";
|
||||
|
||||
if (mediaType.startsWith("image/")) {
|
||||
photo.push(mediaUrl);
|
||||
} else if (mediaType.startsWith("video/")) {
|
||||
video.push(mediaUrl);
|
||||
} else if (mediaType.startsWith("audio/")) {
|
||||
audio.push(mediaUrl);
|
||||
if (mediaType.startsWith("image/")) {
|
||||
photo.push(mediaUrl);
|
||||
} else if (mediaType.startsWith("video/")) {
|
||||
video.push(mediaUrl);
|
||||
} else if (mediaType.startsWith("audio/")) {
|
||||
audio.push(mediaUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Attachment extraction failed — non-critical
|
||||
}
|
||||
|
||||
// In-reply-to
|
||||
const inReplyTo = object.inReplyTo?.href || "";
|
||||
// In-reply-to — Fedify uses replyTargetId (non-fetching)
|
||||
const inReplyTo = object.replyTargetId?.href || "";
|
||||
|
||||
// Build base timeline item
|
||||
const item = {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rmdes/indiekit-endpoint-activitypub",
|
||||
"version": "1.1.8",
|
||||
"version": "1.1.12",
|
||||
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
||||
"keywords": [
|
||||
"indiekit",
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
<a href="{{ replyContext.author.url }}">{{ replyContext.author.name }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if replyContext.content and replyContext.content.text %}
|
||||
{% if replyContext.content and (replyContext.content.html or replyContext.content.text) %}
|
||||
<blockquote class="ap-compose__context-text">
|
||||
{{ replyContext.content.text | truncate(300) }}
|
||||
{{ replyContext.content.html | safe if replyContext.content.html else replyContext.content.text | truncate(300) }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
<a href="{{ replyTo }}" class="ap-compose__context-link" target="_blank" rel="noopener">{{ replyTo }}</a>
|
||||
@@ -74,7 +74,7 @@
|
||||
<legend>{{ __("activitypub.compose.syndicateLabel") }}</legend>
|
||||
{% for target in syndicationTargets %}
|
||||
<label class="ap-compose__syndication-target">
|
||||
<input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}" checked>
|
||||
<input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}" {{ "checked" if target.defaultChecked }}>
|
||||
{{ target.name }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
{% if actorHandle %}
|
||||
<div class="ap-profile__handle">@{{ actorHandle }}@{{ instanceHost }}</div>
|
||||
{% endif %}
|
||||
{% if summary %}
|
||||
<div class="ap-profile__bio">{{ summary | safe }}</div>
|
||||
{% if bio %}
|
||||
<div class="ap-profile__bio">{{ bio | safe }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -97,11 +97,13 @@
|
||||
{% endif %}
|
||||
|
||||
{# Interaction buttons — Alpine.js for optimistic updates #}
|
||||
{# Dynamic data moved to data-* attributes to prevent XSS from inline interpolation #}
|
||||
{# Use canonical AP uid for interactions (Fedify lookupObject), display url for links #}
|
||||
{% set itemUrl = item.url or item.originalUrl %}
|
||||
{% set isLiked = interactionMap[itemUrl].like if interactionMap[itemUrl] else false %}
|
||||
{% set isBoosted = interactionMap[itemUrl].boost if interactionMap[itemUrl] else false %}
|
||||
{% set itemUid = item.uid or item.url or item.originalUrl %}
|
||||
{% set isLiked = interactionMap[itemUid].like if interactionMap[itemUid] else false %}
|
||||
{% set isBoosted = interactionMap[itemUid].boost if interactionMap[itemUid] else false %}
|
||||
<footer class="ap-card__actions"
|
||||
data-item-uid="{{ itemUid }}"
|
||||
data-item-url="{{ itemUrl }}"
|
||||
data-csrf-token="{{ csrfToken }}"
|
||||
data-mount-path="{{ mountPath }}"
|
||||
@@ -115,7 +117,7 @@
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
const el = this.$root;
|
||||
const itemUrl = el.dataset.itemUrl;
|
||||
const itemUid = el.dataset.itemUid;
|
||||
const csrfToken = el.dataset.csrfToken;
|
||||
const basePath = el.dataset.mountPath;
|
||||
const prev = { liked: this.liked, boosted: this.boosted };
|
||||
@@ -130,7 +132,7 @@
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ url: itemUrl })
|
||||
body: JSON.stringify({ url: itemUid })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) {
|
||||
@@ -147,7 +149,7 @@
|
||||
if (this.error) setTimeout(() => this.error = '', 3000);
|
||||
}
|
||||
}">
|
||||
<a href="{{ mountPath }}/admin/reader/compose?replyTo={{ itemUrl | urlencode }}"
|
||||
<a href="{{ mountPath }}/admin/reader/compose?replyTo={{ itemUid | urlencode }}"
|
||||
class="ap-card__action ap-card__action--reply"
|
||||
title="{{ __('activitypub.reader.actions.reply') }}">
|
||||
↩ {{ __("activitypub.reader.actions.reply") }}
|
||||
|
||||
Reference in New Issue
Block a user