@rmdes/indiekit-endpoint-microsub 1.0.55 → 1.0.57

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.
Files changed (75) hide show
  1. package/assets/reader.js +408 -0
  2. package/index.js +61 -49
  3. package/lib/activitypub/outbox-fetcher.js +14 -2
  4. package/lib/cache/redis.js +26 -7
  5. package/lib/controllers/channels.js +2 -2
  6. package/lib/controllers/reader/actor.js +142 -0
  7. package/lib/controllers/reader/channel.js +301 -0
  8. package/lib/controllers/reader/compose.js +242 -0
  9. package/lib/controllers/reader/deck.js +129 -0
  10. package/lib/controllers/reader/feed-repair.js +117 -0
  11. package/lib/controllers/reader/feed.js +246 -0
  12. package/lib/controllers/reader/index.js +126 -0
  13. package/lib/controllers/reader/search.js +157 -0
  14. package/lib/controllers/reader/timeline.js +250 -0
  15. package/lib/controllers/search.js +6 -0
  16. package/lib/controllers/timeline.js +6 -4
  17. package/lib/feeds/atom.js +1 -1
  18. package/lib/feeds/capabilities.js +5 -0
  19. package/lib/feeds/fetcher.js +5 -28
  20. package/lib/feeds/hfeed.js +1 -1
  21. package/lib/feeds/jsonfeed.js +1 -1
  22. package/lib/feeds/normalizer-hfeed.js +209 -0
  23. package/lib/feeds/normalizer-jsonfeed.js +171 -0
  24. package/lib/feeds/normalizer-rss.js +178 -0
  25. package/lib/feeds/normalizer.js +22 -614
  26. package/lib/feeds/rss.js +1 -1
  27. package/lib/media/proxy.js +82 -27
  28. package/lib/polling/processor.js +30 -21
  29. package/lib/polling/scheduler.js +2 -0
  30. package/lib/realtime/broker.js +6 -1
  31. package/lib/storage/channels.js +53 -42
  32. package/lib/storage/feeds.js +3 -1
  33. package/lib/storage/items-read-state.js +287 -0
  34. package/lib/storage/items-retention.js +174 -0
  35. package/lib/storage/items-search.js +34 -0
  36. package/lib/storage/items.js +113 -610
  37. package/lib/storage/read-state.js +1 -1
  38. package/lib/utils/async-handler.js +7 -0
  39. package/lib/utils/constants.js +7 -0
  40. package/lib/utils/csrf.js +51 -0
  41. package/lib/utils/html.js +25 -0
  42. package/lib/utils/sanitize.js +61 -0
  43. package/lib/utils/source-type.js +28 -0
  44. package/lib/utils/validation.js +8 -2
  45. package/lib/webmention/processor.js +1 -1
  46. package/lib/webmention/verifier.js +10 -21
  47. package/lib/websub/subscriber.js +12 -0
  48. package/locales/de.json +3 -0
  49. package/locales/en.json +2 -0
  50. package/locales/es-419.json +3 -0
  51. package/locales/es.json +3 -0
  52. package/locales/fr.json +3 -0
  53. package/locales/hi.json +3 -0
  54. package/locales/id.json +3 -0
  55. package/locales/it.json +3 -0
  56. package/locales/nl.json +3 -0
  57. package/locales/pl.json +3 -0
  58. package/locales/pt-BR.json +3 -0
  59. package/locales/pt.json +3 -0
  60. package/locales/sr.json +3 -0
  61. package/locales/sv.json +3 -0
  62. package/locales/zh-Hans-CN.json +3 -0
  63. package/package.json +3 -1
  64. package/views/actor.njk +2 -0
  65. package/views/channel-new.njk +1 -0
  66. package/views/channel.njk +3 -344
  67. package/views/compose.njk +1 -0
  68. package/views/deck-settings.njk +1 -0
  69. package/views/feed-edit.njk +3 -0
  70. package/views/feeds.njk +4 -0
  71. package/views/layouts/reader.njk +1 -0
  72. package/views/search.njk +2 -0
  73. package/views/settings.njk +2 -0
  74. package/views/timeline.njk +3 -271
  75. package/lib/controllers/reader.js +0 -1580
@@ -0,0 +1,171 @@
1
+ /**
2
+ * JSON Feed normalization
3
+ * @module feeds/normalizer-jsonfeed
4
+ */
5
+
6
+ import {
7
+ generateItemUid,
8
+ extractImagesFromHtml,
9
+ sanitizeHtml,
10
+ SANITIZE_OPTIONS,
11
+ } from "./normalizer.js";
12
+
13
+ /**
14
+ * Normalize JSON Feed item
15
+ * @param {object} item - JSON Feed item
16
+ * @param {string} feedUrl - Feed URL
17
+ * @returns {object} Normalized jf2 item
18
+ */
19
+ export function normalizeJsonFeedItem(item, feedUrl) {
20
+ const url = item.url || item.external_url;
21
+ const uid = generateItemUid(feedUrl, item.id || url);
22
+
23
+ const normalized = {
24
+ type: "entry",
25
+ uid,
26
+ url,
27
+ name: item.title
28
+ ? sanitizeHtml(item.title, { allowedTags: [] }).trim()
29
+ : undefined,
30
+ published: item.date_published
31
+ ? new Date(item.date_published).toISOString()
32
+ : undefined,
33
+ updated: item.date_modified
34
+ ? new Date(item.date_modified).toISOString()
35
+ : undefined,
36
+ _source: {
37
+ url: feedUrl,
38
+ feedUrl,
39
+ feedType: "jsonfeed",
40
+ originalId: item.id,
41
+ },
42
+ };
43
+
44
+ // Content
45
+ if (item.content_html || item.content_text) {
46
+ normalized.content = {};
47
+ if (item.content_html) {
48
+ normalized.content.html = sanitizeHtml(
49
+ item.content_html,
50
+ SANITIZE_OPTIONS,
51
+ );
52
+ normalized.content.text = sanitizeHtml(item.content_html, {
53
+ allowedTags: [],
54
+ }).trim();
55
+ } else if (item.content_text) {
56
+ normalized.content.text = item.content_text;
57
+ }
58
+ }
59
+
60
+ // Summary
61
+ if (item.summary) {
62
+ normalized.summary = item.summary;
63
+ }
64
+
65
+ // Author
66
+ if (item.author || item.authors) {
67
+ const author = item.author || (item.authors && item.authors[0]);
68
+ if (author) {
69
+ normalized.author = {
70
+ type: "card",
71
+ name: author.name,
72
+ url: author.url,
73
+ photo: author.avatar,
74
+ };
75
+ }
76
+ }
77
+
78
+ // Tags
79
+ if (item.tags && item.tags.length > 0) {
80
+ normalized.category = item.tags;
81
+ }
82
+
83
+ // Featured image
84
+ if (item.image) {
85
+ normalized.photo = [item.image];
86
+ }
87
+
88
+ if (item.banner_image && !normalized.photo) {
89
+ normalized.photo = [item.banner_image];
90
+ }
91
+
92
+ // Attachments
93
+ if (item.attachments && item.attachments.length > 0) {
94
+ for (const attachment of item.attachments) {
95
+ const mediaType = attachment.mime_type || "";
96
+
97
+ if (mediaType.startsWith("image/")) {
98
+ normalized.photo = normalized.photo || [];
99
+ normalized.photo.push(attachment.url);
100
+ } else if (mediaType.startsWith("video/")) {
101
+ normalized.video = normalized.video || [];
102
+ normalized.video.push(attachment.url);
103
+ } else if (mediaType.startsWith("audio/")) {
104
+ normalized.audio = normalized.audio || [];
105
+ normalized.audio.push(attachment.url);
106
+ }
107
+ }
108
+ }
109
+
110
+ // External URL
111
+ if (item.external_url && item.url !== item.external_url) {
112
+ normalized["bookmark-of"] = [item.external_url];
113
+ }
114
+
115
+ // Extract images from HTML content as fallback
116
+ if (!normalized.photo && normalized.content?.html) {
117
+ const extracted = extractImagesFromHtml(normalized.content.html);
118
+ if (extracted.length > 0) {
119
+ normalized.photo = extracted;
120
+ }
121
+ }
122
+
123
+ return normalized;
124
+ }
125
+
126
+ /**
127
+ * Normalize JSON Feed metadata
128
+ * @param {object} feed - JSON Feed object
129
+ * @param {string} feedUrl - Feed URL
130
+ * @returns {object} Normalized feed metadata
131
+ */
132
+ export function normalizeJsonFeedMeta(feed, feedUrl) {
133
+ const normalized = {
134
+ name: feed.title
135
+ ? sanitizeHtml(feed.title, { allowedTags: [] }).trim()
136
+ : feedUrl,
137
+ };
138
+
139
+ if (feed.description) {
140
+ normalized.summary = feed.description;
141
+ }
142
+
143
+ if (feed.home_page_url) {
144
+ normalized.url = feed.home_page_url;
145
+ }
146
+
147
+ if (feed.icon) {
148
+ normalized.photo = feed.icon;
149
+ } else if (feed.favicon) {
150
+ normalized.photo = feed.favicon;
151
+ }
152
+
153
+ if (feed.author || feed.authors) {
154
+ const author = feed.author || (feed.authors && feed.authors[0]);
155
+ if (author) {
156
+ normalized.author = {
157
+ type: "card",
158
+ name: author.name,
159
+ url: author.url,
160
+ photo: author.avatar,
161
+ };
162
+ }
163
+ }
164
+
165
+ // Hub for WebSub
166
+ if (feed.hubs && feed.hubs.length > 0) {
167
+ normalized._hub = feed.hubs[0].url;
168
+ }
169
+
170
+ return normalized;
171
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * RSS/Atom feed normalization
3
+ * @module feeds/normalizer-rss
4
+ */
5
+
6
+ import {
7
+ generateItemUid,
8
+ toISOStringSafe,
9
+ extractImagesFromHtml,
10
+ sanitizeHtml,
11
+ SANITIZE_OPTIONS,
12
+ } from "./normalizer.js";
13
+
14
+ /**
15
+ * Normalize RSS/Atom item from feedparser
16
+ * @param {object} item - Feedparser item
17
+ * @param {string} feedUrl - Feed URL
18
+ * @param {string} feedType - 'rss' or 'atom'
19
+ * @returns {object} Normalized jf2 item
20
+ */
21
+ export function normalizeItem(item, feedUrl, feedType) {
22
+ const url = item.link || item.origlink || item.guid;
23
+ const uid = generateItemUid(feedUrl, item.guid || url || item.title);
24
+
25
+ const normalized = {
26
+ type: "entry",
27
+ uid,
28
+ url,
29
+ name: item.title
30
+ ? sanitizeHtml(item.title, { allowedTags: [] }).trim()
31
+ : undefined,
32
+ published: toISOStringSafe(item.pubdate),
33
+ updated: toISOStringSafe(item.date),
34
+ _source: {
35
+ url: feedUrl,
36
+ feedUrl,
37
+ feedType,
38
+ originalId: item.guid,
39
+ },
40
+ };
41
+
42
+ // Content
43
+ if (item.description || item.summary) {
44
+ const html = item.description || item.summary;
45
+ normalized.content = {
46
+ html: sanitizeHtml(html, SANITIZE_OPTIONS),
47
+ text: sanitizeHtml(html, { allowedTags: [] }).trim(),
48
+ };
49
+ }
50
+
51
+ // Summary (prefer explicit summary over truncated content)
52
+ if (item.summary && item.description && item.summary !== item.description) {
53
+ normalized.summary = sanitizeHtml(item.summary, { allowedTags: [] }).trim();
54
+ }
55
+
56
+ // Author
57
+ if (item.author || item["dc:creator"]) {
58
+ const authorName = item.author || item["dc:creator"];
59
+ normalized.author = {
60
+ type: "card",
61
+ name: authorName,
62
+ };
63
+ }
64
+
65
+ // Categories/tags
66
+ if (item.categories && item.categories.length > 0) {
67
+ normalized.category = item.categories;
68
+ }
69
+
70
+ // Enclosures (media)
71
+ if (item.enclosures && item.enclosures.length > 0) {
72
+ for (const enclosure of item.enclosures) {
73
+ const mediaUrl = enclosure.url;
74
+ const mediaType = enclosure.type || "";
75
+
76
+ if (mediaType.startsWith("image/")) {
77
+ normalized.photo = normalized.photo || [];
78
+ normalized.photo.push(mediaUrl);
79
+ } else if (mediaType.startsWith("video/")) {
80
+ normalized.video = normalized.video || [];
81
+ normalized.video.push(mediaUrl);
82
+ } else if (mediaType.startsWith("audio/")) {
83
+ normalized.audio = normalized.audio || [];
84
+ normalized.audio.push(mediaUrl);
85
+ }
86
+ }
87
+ }
88
+
89
+ // Featured image from media content
90
+ if (item["media:content"] && item["media:content"].url) {
91
+ const mediaType = item["media:content"].type || "";
92
+ if (
93
+ mediaType.startsWith("image/") ||
94
+ item["media:content"].medium === "image"
95
+ ) {
96
+ normalized.photo = normalized.photo || [];
97
+ if (!normalized.photo.includes(item["media:content"].url)) {
98
+ normalized.photo.push(item["media:content"].url);
99
+ }
100
+ }
101
+ }
102
+
103
+ // Image from item.image
104
+ if (item.image && item.image.url) {
105
+ normalized.photo = normalized.photo || [];
106
+ if (!normalized.photo.includes(item.image.url)) {
107
+ normalized.photo.push(item.image.url);
108
+ }
109
+ }
110
+
111
+ // Extract images from HTML content as fallback
112
+ if (!normalized.photo && normalized.content?.html) {
113
+ const extracted = extractImagesFromHtml(normalized.content.html);
114
+ if (extracted.length > 0) {
115
+ normalized.photo = extracted;
116
+ }
117
+ }
118
+
119
+ return normalized;
120
+ }
121
+
122
+ /**
123
+ * Normalize feed metadata from feedparser
124
+ * @param {object} meta - Feedparser meta object
125
+ * @param {string} feedUrl - Feed URL
126
+ * @returns {object} Normalized feed metadata
127
+ */
128
+ export function normalizeFeedMeta(meta, feedUrl) {
129
+ const normalized = {
130
+ name: meta.title
131
+ ? sanitizeHtml(meta.title, { allowedTags: [] }).trim()
132
+ : feedUrl,
133
+ };
134
+
135
+ if (meta.description) {
136
+ normalized.summary = meta.description;
137
+ }
138
+
139
+ if (meta.link) {
140
+ normalized.url = meta.link;
141
+ }
142
+
143
+ if (meta.image && meta.image.url) {
144
+ normalized.photo = meta.image.url;
145
+ }
146
+
147
+ if (meta.favicon) {
148
+ normalized.photo = normalized.photo || meta.favicon;
149
+ }
150
+
151
+ // Author/publisher
152
+ if (meta.author) {
153
+ normalized.author = {
154
+ type: "card",
155
+ name: meta.author,
156
+ };
157
+ }
158
+
159
+ // Hub for WebSub
160
+ if (meta.cloud && meta.cloud.href) {
161
+ normalized._hub = meta.cloud.href;
162
+ }
163
+
164
+ // Look for hub in links
165
+ if (meta.link && meta["atom:link"]) {
166
+ const links = Array.isArray(meta["atom:link"])
167
+ ? meta["atom:link"]
168
+ : [meta["atom:link"]];
169
+ for (const link of links) {
170
+ if (link["@"] && link["@"].rel === "hub") {
171
+ normalized._hub = link["@"].href;
172
+ break;
173
+ }
174
+ }
175
+ }
176
+
177
+ return normalized;
178
+ }