From d395a1cc24d1a8306842fbe2dfab4afa0fc40c5a Mon Sep 17 00:00:00 2001 From: Ricardo Date: Sat, 21 Feb 2026 14:54:10 +0100 Subject: [PATCH] fix: resolve Unknown authors, filter empty boosts, style mentions - Add actorFallback option to extractObjectData() so the activity's actor is used when object.getAttributedTo() fails (Authorized Fetch, unreachable servers). Falls back to attributionIds for URL-based info. - Pass create.getActor() as actorFallback in Create inbox listener. - Skip storing boosts with no content (Lemmy/PieFed activity IDs). - Add template guard to hide empty cards already in the database. - Style @mention and hashtag links distinctly from prose content. - Handle Mastodon's invisible/ellipsis URL span classes. --- assets/reader.css | 34 +++++++++++++++++++++++++++++++ lib/inbox-listeners.js | 9 ++++++++- lib/timeline-store.js | 36 ++++++++++++++++++++++++++++++--- package.json | 2 +- views/partials/ap-item-card.njk | 8 ++++++++ 5 files changed, 84 insertions(+), 5 deletions(-) diff --git a/assets/reader.css b/assets/reader.css index fe5e3be..1476b11 100644 --- a/assets/reader.css +++ b/assets/reader.css @@ -296,6 +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; +} + +.ap-card__content a.u-url.mention:hover { + color: var(--color-primary); + text-decoration: underline; +} + +/* Hashtag mentions — subtle tag styling */ +.ap-card__content a.mention.hashtag { + color: var(--color-on-offset); + font-size: var(--font-size-s); + text-decoration: none; +} + +.ap-card__content a.mention.hashtag:hover { + color: var(--color-primary); + text-decoration: underline; +} + +/* Mastodon's invisible/ellipsis spans for long URLs */ +.ap-card__content .invisible { + display: none; +} + +.ap-card__content .ellipsis::after { + content: "…"; +} + /* ========================================================================== Content Warning ========================================================================== */ diff --git a/lib/inbox-listeners.js b/lib/inbox-listeners.js index 94161dc..e3400a0 100644 --- a/lib/inbox-listeners.js +++ b/lib/inbox-listeners.js @@ -321,6 +321,11 @@ export function registerInboxListeners(inboxChain, options) { const object = await announce.getObject(); if (!object) return; + // Skip non-content objects (Lemmy/PieFed like/create activities + // that resolve to activity IDs instead of actual Note/Article posts) + const hasContent = object.content?.toString() || object.name?.toString(); + if (!hasContent) return; + // Get booster actor info const boosterActor = await announce.getActor(); const boosterInfo = await extractActorInfo(boosterActor); @@ -446,7 +451,9 @@ export function registerInboxListeners(inboxChain, options) { const following = await collections.ap_following.findOne({ actorUrl }); if (following) { try { - const timelineItem = await extractObjectData(object); + const timelineItem = await extractObjectData(object, { + actorFallback: actorObj, + }); await addTimelineItem(collections, timelineItem); } catch (error) { // Log extraction errors but don't fail the entire handler diff --git a/lib/timeline-store.js b/lib/timeline-store.js index c16adc3..8537acb 100644 --- a/lib/timeline-store.js +++ b/lib/timeline-store.js @@ -87,6 +87,7 @@ export async function extractActorInfo(actor) { * @param {object} options - Extraction options * @param {object} [options.boostedBy] - Actor info for boosts * @param {Date} [options.boostedAt] - Boost timestamp + * @param {object} [options.actorFallback] - Fedify actor to use when object.getAttributedTo() fails * @returns {Promise} Timeline item data */ export async function extractObjectData(object, options = {}) { @@ -127,7 +128,7 @@ export async function extractObjectData(object, options = {}) { ? String(object.published) : new Date().toISOString(); - // Extract author — use async getAttributedTo() for Fedify objects + // Extract author — try multiple strategies in order of reliability let authorObj = null; try { if (typeof object.getAttributedTo === "function") { @@ -135,10 +136,39 @@ export async function extractObjectData(object, options = {}) { authorObj = Array.isArray(attr) ? attr[0] : attr; } } catch { - // Fallback: try direct property access for plain objects + // getAttributedTo() failed (Authorized Fetch, unreachable, etc.) + } + // If getAttributedTo() returned nothing, use the actor from the wrapping activity + if (!authorObj && options.actorFallback) { + authorObj = options.actorFallback; + } + // Try direct property access for plain objects + if (!authorObj) { authorObj = object.attribution || object.attributedTo || null; } - const author = await extractActorInfo(authorObj); + + let author; + if (authorObj) { + author = await extractActorInfo(authorObj); + } else { + // Last resort: use attributionIds (non-fetching) to get at least a URL + const attrIds = object.attributionIds; + if (attrIds && attrIds.length > 0) { + const authorUrl = attrIds[0].href; + const authorHostname = new URL(authorUrl).hostname; + // Extract username from URL pattern like /users/name or /@name + const pathMatch = new URL(authorUrl).pathname.match(/\/@?([^/]+)/); + const username = pathMatch ? pathMatch[1] : ""; + author = { + name: username || authorHostname, + url: authorUrl, + photo: "", + handle: username ? `@${username}@${authorHostname}` : "", + }; + } else { + author = { name: "Unknown", url: "", photo: "", handle: "" }; + } + } // Extract tags/categories const category = []; diff --git a/package.json b/package.json index 32b58e9..8ea9c87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-activitypub", - "version": "1.1.5", + "version": "1.1.6", "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/partials/ap-item-card.njk b/views/partials/ap-item-card.njk index 44d3ebb..86858e7 100644 --- a/views/partials/ap-item-card.njk +++ b/views/partials/ap-item-card.njk @@ -1,5 +1,11 @@ {# Timeline item card partial - reusable across timeline and profile views #} +{# Skip empty cards (e.g. Lemmy/PieFed activity IDs with no actual content) #} +{% set hasCardContent = item.content and (item.content.html or item.content.text) %} +{% set hasCardTitle = item.name %} +{% set hasCardMedia = (item.photo and item.photo.length > 0) or (item.video and item.video.length > 0) or (item.audio and item.audio.length > 0) %} +{% if hasCardContent or hasCardTitle or hasCardMedia %} +
{# Boost header if this is a boosted post #} {% if item.type == "boost" and item.boostedBy %} @@ -167,3 +173,5 @@
+ +{% endif %}{# end hasCardContent/hasCardTitle/hasCardMedia guard #}