mirror of
https://github.com/svemagie/indiekit-endpoint-activitypub.git
synced 2026-04-02 15:44:58 +02:00
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.
This commit is contained in:
@@ -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
|
||||
========================================================================== */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<object>} 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 = [];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
<article class="ap-card{% if item.type %} ap-card--{{ item.type }}{% endif %}{% if item.inReplyTo %} ap-card--reply{% endif %}">
|
||||
{# Boost header if this is a boosted post #}
|
||||
{% if item.type == "boost" and item.boostedBy %}
|
||||
@@ -167,3 +173,5 @@
|
||||
<div x-show="error" x-text="error" class="ap-card__action-error" x-transition></div>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
{% endif %}{# end hasCardContent/hasCardTitle/hasCardMedia guard #}
|
||||
|
||||
Reference in New Issue
Block a user