@rmdes/indiekit-endpoint-activitypub 2.5.5 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/reader.css
CHANGED
|
@@ -301,6 +301,21 @@
|
|
|
301
301
|
text-decoration: underline;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
.ap-card__bot-badge {
|
|
305
|
+
display: inline-block;
|
|
306
|
+
font-size: 0.6rem;
|
|
307
|
+
font-weight: 700;
|
|
308
|
+
line-height: 1;
|
|
309
|
+
padding: 0.15em 0.35em;
|
|
310
|
+
margin-left: 0.3em;
|
|
311
|
+
border: var(--border-width-thin) solid var(--color-on-offset);
|
|
312
|
+
border-radius: var(--border-radius-small);
|
|
313
|
+
color: var(--color-on-offset);
|
|
314
|
+
vertical-align: middle;
|
|
315
|
+
text-transform: uppercase;
|
|
316
|
+
letter-spacing: 0.03em;
|
|
317
|
+
}
|
|
318
|
+
|
|
304
319
|
.ap-card__author-handle {
|
|
305
320
|
color: var(--color-on-offset);
|
|
306
321
|
font-size: var(--font-size-s);
|
|
@@ -315,9 +330,17 @@
|
|
|
315
330
|
font-size: var(--font-size-xs);
|
|
316
331
|
}
|
|
317
332
|
|
|
333
|
+
.ap-card__edited {
|
|
334
|
+
font-size: var(--font-size-xs);
|
|
335
|
+
margin-left: 0.2em;
|
|
336
|
+
}
|
|
337
|
+
|
|
318
338
|
.ap-card__timestamp-link {
|
|
319
339
|
color: inherit;
|
|
320
340
|
text-decoration: none;
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
gap: 0;
|
|
321
344
|
}
|
|
322
345
|
|
|
323
346
|
.ap-card__timestamp-link:hover {
|
|
@@ -706,6 +729,30 @@
|
|
|
706
729
|
opacity: 0.7;
|
|
707
730
|
}
|
|
708
731
|
|
|
732
|
+
/* Hashtag stuffing collapse */
|
|
733
|
+
.ap-hashtag-overflow {
|
|
734
|
+
margin: var(--space-xs) 0;
|
|
735
|
+
font-size: var(--font-size-s);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.ap-hashtag-overflow summary {
|
|
739
|
+
cursor: pointer;
|
|
740
|
+
color: var(--color-on-offset);
|
|
741
|
+
list-style: none;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.ap-hashtag-overflow summary::before {
|
|
745
|
+
content: "▸ ";
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.ap-hashtag-overflow[open] summary::before {
|
|
749
|
+
content: "▾ ";
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.ap-hashtag-overflow p {
|
|
753
|
+
margin-top: var(--space-xs);
|
|
754
|
+
}
|
|
755
|
+
|
|
709
756
|
/* ==========================================================================
|
|
710
757
|
Interaction Buttons
|
|
711
758
|
========================================================================== */
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content post-processing utilities.
|
|
3
|
+
* Applied after sanitization and emoji replacement in the item pipeline.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shorten displayed URLs in <a> tags that exceed maxLength.
|
|
8
|
+
* Keeps the full URL in href, only truncates the visible text.
|
|
9
|
+
*
|
|
10
|
+
* Example: <a href="https://example.com/very/long/path">https://example.com/very/long/path</a>
|
|
11
|
+
* → <a href="https://example.com/very/long/path" title="https://example.com/very/long/path">example.com/very/lon…</a>
|
|
12
|
+
*
|
|
13
|
+
* @param {string} html - Sanitized HTML content
|
|
14
|
+
* @param {number} [maxLength=30] - Max visible URL length before truncation
|
|
15
|
+
* @returns {string} HTML with shortened display URLs
|
|
16
|
+
*/
|
|
17
|
+
export function shortenDisplayUrls(html, maxLength = 30) {
|
|
18
|
+
if (!html) return html;
|
|
19
|
+
|
|
20
|
+
// Match <a ...>URL text</a> where the visible text looks like a URL
|
|
21
|
+
return html.replace(
|
|
22
|
+
/(<a\s[^>]*>)(https?:\/\/[^<]+)(<\/a>)/gi,
|
|
23
|
+
(match, openTag, urlText, closeTag) => {
|
|
24
|
+
if (urlText.length <= maxLength) return match;
|
|
25
|
+
|
|
26
|
+
// Strip protocol for display
|
|
27
|
+
const display = urlText.replace(/^https?:\/\//, "");
|
|
28
|
+
const truncated = display.slice(0, maxLength - 1) + "\u2026";
|
|
29
|
+
|
|
30
|
+
// Add title attribute with full URL for hover tooltip (if not already present)
|
|
31
|
+
let tag = openTag;
|
|
32
|
+
if (!tag.includes("title=")) {
|
|
33
|
+
tag = tag.replace(/>$/, ` title="${urlText}">`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `${tag}${truncated}${closeTag}`;
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Collapse paragraphs that are mostly hashtag links (hashtag stuffing).
|
|
43
|
+
* Detects <p> blocks where 80%+ of the text content is hashtag links
|
|
44
|
+
* and wraps them in a <details> element.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} html - Sanitized HTML content
|
|
47
|
+
* @param {number} [minTags=3] - Minimum number of hashtag links to trigger collapse
|
|
48
|
+
* @returns {string} HTML with hashtag-heavy paragraphs collapsed
|
|
49
|
+
*/
|
|
50
|
+
export function collapseHashtagStuffing(html, minTags = 3) {
|
|
51
|
+
if (!html) return html;
|
|
52
|
+
|
|
53
|
+
// Match <p> blocks
|
|
54
|
+
return html.replace(/<p>([^]*?)<\/p>/gi, (match, inner) => {
|
|
55
|
+
// Count hashtag links: <a ...>#something</a> or plain #word
|
|
56
|
+
const hashtagLinks = inner.match(/<a[^>]*>#[^<]+<\/a>/gi) || [];
|
|
57
|
+
if (hashtagLinks.length < minTags) return match;
|
|
58
|
+
|
|
59
|
+
// Calculate what fraction of text content is hashtags
|
|
60
|
+
const textOnly = inner.replace(/<[^>]*>/g, "").trim();
|
|
61
|
+
const hashtagText = hashtagLinks
|
|
62
|
+
.map((link) => link.replace(/<[^>]*>/g, "").trim())
|
|
63
|
+
.join(" ");
|
|
64
|
+
|
|
65
|
+
// If hashtags make up 80%+ of the text content, collapse
|
|
66
|
+
if (hashtagText.length / Math.max(textOnly.length, 1) >= 0.8) {
|
|
67
|
+
return `<details class="ap-hashtag-overflow"><summary>Show ${hashtagLinks.length} tags</summary><p>${inner}</p></details>`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return match;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -119,12 +119,14 @@ export function mapMastodonStatusToItem(status, instance) {
|
|
|
119
119
|
summary: status.spoiler_text || "",
|
|
120
120
|
sensitive: status.sensitive || false,
|
|
121
121
|
published: status.created_at || new Date().toISOString(),
|
|
122
|
+
updated: status.edited_at || "",
|
|
122
123
|
author: {
|
|
123
124
|
name: sanitizeHtml(account.display_name || account.username || "Unknown", { allowedTags: [], allowedAttributes: {} }),
|
|
124
125
|
url: account.url || "",
|
|
125
126
|
photo: account.avatar || account.avatar_static || "",
|
|
126
127
|
handle,
|
|
127
128
|
emojis: authorEmojis,
|
|
129
|
+
bot: account.bot || false,
|
|
128
130
|
},
|
|
129
131
|
category,
|
|
130
132
|
mentions,
|
package/lib/item-processing.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { stripQuoteReferenceHtml } from "./og-unfurl.js";
|
|
10
10
|
import { replaceCustomEmoji } from "./emoji-utils.js";
|
|
11
|
+
import { shortenDisplayUrls, collapseHashtagStuffing } from "./content-utils.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Post-process timeline items for rendering.
|
|
@@ -31,7 +32,10 @@ export async function postProcessItems(items, options = {}) {
|
|
|
31
32
|
// 3. Replace custom emoji shortcodes with <img> tags
|
|
32
33
|
applyCustomEmoji(items);
|
|
33
34
|
|
|
34
|
-
// 4.
|
|
35
|
+
// 4. Shorten long URLs and collapse hashtag stuffing in content
|
|
36
|
+
applyContentEnhancements(items);
|
|
37
|
+
|
|
38
|
+
// 5. Build interaction map (likes/boosts) — empty when no collection
|
|
35
39
|
const interactionMap = options.interactionsCol
|
|
36
40
|
? await buildInteractionMap(items, options.interactionsCol)
|
|
37
41
|
: {};
|
|
@@ -154,6 +158,24 @@ function applyCustomEmoji(items) {
|
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Shorten long URLs and collapse hashtag-heavy paragraphs in content.
|
|
163
|
+
* Mutates items in place.
|
|
164
|
+
*
|
|
165
|
+
* @param {Array} items
|
|
166
|
+
*/
|
|
167
|
+
function applyContentEnhancements(items) {
|
|
168
|
+
for (const item of items) {
|
|
169
|
+
if (item.content?.html) {
|
|
170
|
+
item.content.html = shortenDisplayUrls(item.content.html);
|
|
171
|
+
item.content.html = collapseHashtagStuffing(item.content.html);
|
|
172
|
+
}
|
|
173
|
+
if (item.quote?.content?.html) {
|
|
174
|
+
item.quote.content.html = shortenDisplayUrls(item.quote.content.html);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
157
179
|
/**
|
|
158
180
|
* Build interaction map (likes/boosts) for template rendering.
|
|
159
181
|
* Returns { [uid]: { like: true, boost: true } }.
|
package/lib/timeline-store.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @module timeline-store
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { Article, Emoji, Hashtag, Mention } from "@fedify/fedify/vocab";
|
|
6
|
+
import { Article, Application, Emoji, Hashtag, Mention, Service } from "@fedify/fedify/vocab";
|
|
7
7
|
import sanitizeHtml from "sanitize-html";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -101,7 +101,10 @@ export async function extractActorInfo(actor, options = {}) {
|
|
|
101
101
|
// Emoji extraction failed — non-critical
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
// Bot detection — Service and Application actors are automated accounts
|
|
105
|
+
const bot = actor instanceof Service || actor instanceof Application;
|
|
106
|
+
|
|
107
|
+
return { name, url, photo, handle, emojis, bot };
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/**
|
|
@@ -154,6 +157,9 @@ export async function extractObjectData(object, options = {}) {
|
|
|
154
157
|
? String(object.published)
|
|
155
158
|
: new Date().toISOString();
|
|
156
159
|
|
|
160
|
+
// Edited date — non-null when the post has been updated after publishing
|
|
161
|
+
const updated = object.updated ? String(object.updated) : "";
|
|
162
|
+
|
|
157
163
|
// Extract author — try multiple strategies in order of reliability
|
|
158
164
|
const loaderOpts = options.documentLoader ? { documentLoader: options.documentLoader } : {};
|
|
159
165
|
let authorObj = null;
|
|
@@ -304,6 +310,7 @@ export async function extractObjectData(object, options = {}) {
|
|
|
304
310
|
summary,
|
|
305
311
|
sensitive,
|
|
306
312
|
published,
|
|
313
|
+
updated,
|
|
307
314
|
author,
|
|
308
315
|
category,
|
|
309
316
|
mentions,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-activitypub",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
{% else %}
|
|
54
54
|
<span>{% if item.author.nameHtml %}{{ item.author.nameHtml | safe }}{% else %}{{ item.author.name or "Unknown" }}{% endif %}</span>
|
|
55
55
|
{% endif %}
|
|
56
|
+
{% if item.author.bot %}<span class="ap-card__bot-badge" title="Bot account">BOT</span>{% endif %}
|
|
56
57
|
</div>
|
|
57
58
|
{% if item.author.handle %}
|
|
58
59
|
<div class="ap-card__author-handle">{{ item.author.handle }}</div>
|
|
@@ -63,6 +64,7 @@
|
|
|
63
64
|
<time datetime="{{ item.published }}" class="ap-card__timestamp" x-data x-relative-time>
|
|
64
65
|
{{ item.published | date("PPp") }}
|
|
65
66
|
</time>
|
|
67
|
+
{% if item.updated %}<span class="ap-card__edited" title="{{ item.updated | date('PPp') }}">✏️</span>{% endif %}
|
|
66
68
|
</a>
|
|
67
69
|
{% endif %}
|
|
68
70
|
</header>
|