@rmdes/indiekit-endpoint-microsub 1.0.26 → 1.0.28
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/lib/controllers/block.js +1 -1
- package/lib/controllers/follow.js +17 -0
- package/lib/controllers/mute.js +1 -1
- package/lib/storage/channels.js +6 -6
- package/lib/storage/feeds.js +13 -13
- package/lib/storage/items.js +5 -5
- package/lib/utils/blogroll-notify.js +119 -0
- package/lib/webmention/processor.js +4 -4
- package/package.json +1 -1
package/lib/controllers/block.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
getFeedsForChannel,
|
|
15
15
|
} from "../storage/feeds.js";
|
|
16
16
|
import { getUserId } from "../utils/auth.js";
|
|
17
|
+
import { notifyBlogroll } from "../utils/blogroll-notify.js";
|
|
17
18
|
import { createFeedResponse } from "../utils/jf2.js";
|
|
18
19
|
import { validateChannel, validateUrl } from "../utils/validation.js";
|
|
19
20
|
import {
|
|
@@ -78,6 +79,17 @@ export async function follow(request, response) {
|
|
|
78
79
|
console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
// Notify blogroll plugin (fire-and-forget)
|
|
83
|
+
notifyBlogroll(application, "follow", {
|
|
84
|
+
url,
|
|
85
|
+
title: feed.title,
|
|
86
|
+
channelName: channelDocument.name,
|
|
87
|
+
feedId: feed._id.toString(),
|
|
88
|
+
channelId: channelDocument._id.toString(),
|
|
89
|
+
}).catch((error) => {
|
|
90
|
+
console.error(`[Microsub] Blogroll notify error:`, error.message);
|
|
91
|
+
});
|
|
92
|
+
|
|
81
93
|
response.status(201).json(createFeedResponse(feed));
|
|
82
94
|
}
|
|
83
95
|
|
|
@@ -122,6 +134,11 @@ export async function unfollow(request, response) {
|
|
|
122
134
|
throw new IndiekitError("Feed not found", { status: 404 });
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
// Notify blogroll plugin (fire-and-forget)
|
|
138
|
+
notifyBlogroll(application, "unfollow", { url }).catch((error) => {
|
|
139
|
+
console.error(`[Microsub] Blogroll notify error:`, error.message);
|
|
140
|
+
});
|
|
141
|
+
|
|
125
142
|
response.json({ result: "ok" });
|
|
126
143
|
}
|
|
127
144
|
|
package/lib/controllers/mute.js
CHANGED
package/lib/storage/channels.js
CHANGED
|
@@ -74,8 +74,8 @@ export async function createChannel(application, { name, userId }) {
|
|
|
74
74
|
excludeTypes: [],
|
|
75
75
|
excludeRegex: undefined,
|
|
76
76
|
},
|
|
77
|
-
createdAt: new Date(),
|
|
78
|
-
updatedAt: new Date(),
|
|
77
|
+
createdAt: new Date().toISOString(),
|
|
78
|
+
updatedAt: new Date().toISOString(),
|
|
79
79
|
};
|
|
80
80
|
|
|
81
81
|
await collection.insertOne(channel);
|
|
@@ -185,7 +185,7 @@ export async function updateChannel(application, uid, updates, userId) {
|
|
|
185
185
|
{
|
|
186
186
|
$set: {
|
|
187
187
|
...updates,
|
|
188
|
-
updatedAt: new Date(),
|
|
188
|
+
updatedAt: new Date().toISOString(),
|
|
189
189
|
},
|
|
190
190
|
},
|
|
191
191
|
{ returnDocument: "after" },
|
|
@@ -242,7 +242,7 @@ export async function reorderChannels(application, channelUids, userId) {
|
|
|
242
242
|
const operations = channelUids.map((uid, index) => ({
|
|
243
243
|
updateOne: {
|
|
244
244
|
filter: userId ? { uid, userId } : { uid },
|
|
245
|
-
update: { $set: { order: index, updatedAt: new Date() } },
|
|
245
|
+
update: { $set: { order: index, updatedAt: new Date().toISOString() } },
|
|
246
246
|
},
|
|
247
247
|
}));
|
|
248
248
|
|
|
@@ -298,8 +298,8 @@ export async function ensureNotificationsChannel(application, userId) {
|
|
|
298
298
|
excludeTypes: [],
|
|
299
299
|
excludeRegex: undefined,
|
|
300
300
|
},
|
|
301
|
-
createdAt: new Date(),
|
|
302
|
-
updatedAt: new Date(),
|
|
301
|
+
createdAt: new Date().toISOString(),
|
|
302
|
+
updatedAt: new Date().toISOString(),
|
|
303
303
|
};
|
|
304
304
|
|
|
305
305
|
await collection.insertOne(channel);
|
package/lib/storage/feeds.js
CHANGED
|
@@ -45,11 +45,11 @@ export async function createFeed(
|
|
|
45
45
|
photo: photo || undefined,
|
|
46
46
|
tier: 1, // Start at tier 1 (2 minutes)
|
|
47
47
|
unmodified: 0,
|
|
48
|
-
nextFetchAt: new Date(), // Fetch immediately
|
|
48
|
+
nextFetchAt: new Date(), // Fetch immediately (kept as Date for query compatibility)
|
|
49
49
|
lastFetchedAt: undefined,
|
|
50
50
|
websub: undefined, // Will be populated if hub is discovered
|
|
51
|
-
createdAt: new Date(),
|
|
52
|
-
updatedAt: new Date(),
|
|
51
|
+
createdAt: new Date().toISOString(),
|
|
52
|
+
updatedAt: new Date().toISOString(),
|
|
53
53
|
};
|
|
54
54
|
|
|
55
55
|
await collection.insertOne(feed);
|
|
@@ -114,7 +114,7 @@ export async function updateFeed(application, id, updates) {
|
|
|
114
114
|
{
|
|
115
115
|
$set: {
|
|
116
116
|
...updates,
|
|
117
|
-
updatedAt: new Date(),
|
|
117
|
+
updatedAt: new Date().toISOString(),
|
|
118
118
|
},
|
|
119
119
|
},
|
|
120
120
|
{ returnDocument: "after" },
|
|
@@ -227,15 +227,15 @@ export async function updateFeedAfterFetch(
|
|
|
227
227
|
updateData = {
|
|
228
228
|
tier,
|
|
229
229
|
unmodified,
|
|
230
|
-
nextFetchAt,
|
|
231
|
-
lastFetchedAt: new Date(),
|
|
232
|
-
updatedAt: new Date(),
|
|
230
|
+
nextFetchAt, // Kept as Date for query compatibility
|
|
231
|
+
lastFetchedAt: new Date().toISOString(),
|
|
232
|
+
updatedAt: new Date().toISOString(),
|
|
233
233
|
};
|
|
234
234
|
} else {
|
|
235
235
|
updateData = {
|
|
236
236
|
...extra,
|
|
237
|
-
lastFetchedAt: new Date(),
|
|
238
|
-
updatedAt: new Date(),
|
|
237
|
+
lastFetchedAt: new Date().toISOString(),
|
|
238
|
+
updatedAt: new Date().toISOString(),
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
|
|
@@ -280,7 +280,7 @@ export async function updateFeedWebsub(application, id, websub) {
|
|
|
280
280
|
{
|
|
281
281
|
$set: {
|
|
282
282
|
websub: websubData,
|
|
283
|
-
updatedAt: new Date(),
|
|
283
|
+
updatedAt: new Date().toISOString(),
|
|
284
284
|
},
|
|
285
285
|
},
|
|
286
286
|
{ returnDocument: "after" },
|
|
@@ -314,12 +314,12 @@ export async function updateFeedStatus(application, id, status) {
|
|
|
314
314
|
const objectId = typeof id === "string" ? new ObjectId(id) : id;
|
|
315
315
|
|
|
316
316
|
const updateFields = {
|
|
317
|
-
updatedAt: new Date(),
|
|
317
|
+
updatedAt: new Date().toISOString(),
|
|
318
318
|
};
|
|
319
319
|
|
|
320
320
|
if (status.success) {
|
|
321
321
|
updateFields.status = "active";
|
|
322
|
-
updateFields.lastSuccessAt = new Date();
|
|
322
|
+
updateFields.lastSuccessAt = new Date().toISOString();
|
|
323
323
|
updateFields.consecutiveErrors = 0;
|
|
324
324
|
updateFields.lastError = undefined;
|
|
325
325
|
updateFields.lastErrorAt = undefined;
|
|
@@ -330,7 +330,7 @@ export async function updateFeedStatus(application, id, status) {
|
|
|
330
330
|
} else {
|
|
331
331
|
updateFields.status = "error";
|
|
332
332
|
updateFields.lastError = status.error;
|
|
333
|
-
updateFields.lastErrorAt = new Date();
|
|
333
|
+
updateFields.lastErrorAt = new Date().toISOString();
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
// Use $set for most fields, $inc for consecutiveErrors on failure
|
package/lib/storage/items.js
CHANGED
|
@@ -49,8 +49,8 @@ export async function addItem(application, { channelId, feedId, uid, item }) {
|
|
|
49
49
|
name: item.name || undefined,
|
|
50
50
|
content: item.content || undefined,
|
|
51
51
|
summary: item.summary || undefined,
|
|
52
|
-
published: item.published ? new Date(item.published) : new Date(),
|
|
53
|
-
updated: item.updated ? new Date(item.updated) : undefined,
|
|
52
|
+
published: item.published ? new Date(item.published) : new Date(), // Keep as Date for query compatibility
|
|
53
|
+
updated: item.updated ? new Date(item.updated) : undefined, // Keep as Date for query compatibility
|
|
54
54
|
author: item.author || undefined,
|
|
55
55
|
category: item.category || [],
|
|
56
56
|
photo: item.photo || [],
|
|
@@ -62,7 +62,7 @@ export async function addItem(application, { channelId, feedId, uid, item }) {
|
|
|
62
62
|
inReplyTo: item["in-reply-to"] || item.inReplyTo || [],
|
|
63
63
|
source: item._source || undefined,
|
|
64
64
|
readBy: [], // Array of user IDs who have read this item
|
|
65
|
-
createdAt: new Date(),
|
|
65
|
+
createdAt: new Date().toISOString(),
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
await collection.insertOne(document);
|
|
@@ -182,7 +182,7 @@ function transformToJf2(item, userId) {
|
|
|
182
182
|
type: item.type,
|
|
183
183
|
uid: item.uid,
|
|
184
184
|
url: item.url,
|
|
185
|
-
published: item.published?.toISOString(),
|
|
185
|
+
published: item.published?.toISOString(), // Convert Date to ISO string
|
|
186
186
|
_id: item._id.toString(),
|
|
187
187
|
_is_read: userId ? item.readBy?.includes(userId) : false,
|
|
188
188
|
};
|
|
@@ -191,7 +191,7 @@ function transformToJf2(item, userId) {
|
|
|
191
191
|
if (item.name) jf2.name = item.name;
|
|
192
192
|
if (item.content) jf2.content = item.content;
|
|
193
193
|
if (item.summary) jf2.summary = item.summary;
|
|
194
|
-
if (item.updated) jf2.updated = item.updated.toISOString();
|
|
194
|
+
if (item.updated) jf2.updated = item.updated.toISOString(); // Convert Date to ISO string
|
|
195
195
|
if (item.author) jf2.author = normalizeAuthor(item.author);
|
|
196
196
|
if (item.category?.length > 0) jf2.category = item.category;
|
|
197
197
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notify blogroll plugin of Microsub follow/unfollow events
|
|
3
|
+
* @module utils/blogroll-notify
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Notify blogroll of a feed subscription change
|
|
8
|
+
* Fire-and-forget — errors are logged but don't block the response
|
|
9
|
+
* @param {object} application - Application instance
|
|
10
|
+
* @param {string} action - "follow" or "unfollow"
|
|
11
|
+
* @param {object} data - Feed data
|
|
12
|
+
* @param {string} data.url - Feed URL
|
|
13
|
+
* @param {string} [data.title] - Feed title
|
|
14
|
+
* @param {string} [data.channelName] - Channel name
|
|
15
|
+
* @param {string} [data.feedId] - Microsub feed ID
|
|
16
|
+
* @param {string} [data.channelId] - Microsub channel ID
|
|
17
|
+
*/
|
|
18
|
+
export async function notifyBlogroll(application, action, data) {
|
|
19
|
+
// Check if blogroll plugin is installed
|
|
20
|
+
if (typeof application.getBlogrollDb !== "function") {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const db = application.getBlogrollDb();
|
|
25
|
+
if (!db) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const collection = db.collection("blogrollBlogs");
|
|
30
|
+
const now = new Date();
|
|
31
|
+
|
|
32
|
+
if (action === "follow") {
|
|
33
|
+
// Skip if this feed was explicitly deleted by the user
|
|
34
|
+
const deleted = await collection.findOne({
|
|
35
|
+
feedUrl: data.url,
|
|
36
|
+
status: "deleted",
|
|
37
|
+
});
|
|
38
|
+
if (deleted) {
|
|
39
|
+
console.log(
|
|
40
|
+
`[Microsub→Blogroll] Skipping follow for ${data.url} — previously deleted by user`,
|
|
41
|
+
);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Upsert the blog entry
|
|
46
|
+
await collection.updateOne(
|
|
47
|
+
{ feedUrl: data.url },
|
|
48
|
+
{
|
|
49
|
+
$set: {
|
|
50
|
+
title: data.title || extractDomain(data.url),
|
|
51
|
+
siteUrl: extractSiteUrl(data.url),
|
|
52
|
+
feedType: "rss",
|
|
53
|
+
category: data.channelName || "Microsub",
|
|
54
|
+
source: "microsub",
|
|
55
|
+
microsubFeedId: data.feedId || null,
|
|
56
|
+
microsubChannelId: data.channelId || null,
|
|
57
|
+
microsubChannelName: data.channelName || null,
|
|
58
|
+
skipItemFetch: true,
|
|
59
|
+
status: "active",
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
},
|
|
62
|
+
$setOnInsert: {
|
|
63
|
+
description: null,
|
|
64
|
+
tags: [],
|
|
65
|
+
photo: null,
|
|
66
|
+
author: null,
|
|
67
|
+
lastFetchAt: null,
|
|
68
|
+
lastError: null,
|
|
69
|
+
itemCount: 0,
|
|
70
|
+
pinned: false,
|
|
71
|
+
hidden: false,
|
|
72
|
+
notes: null,
|
|
73
|
+
createdAt: now,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{ upsert: true },
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
console.log(`[Microsub→Blogroll] Added/updated feed ${data.url}`);
|
|
80
|
+
} else if (action === "unfollow") {
|
|
81
|
+
// Soft-delete the blog entry if it came from microsub
|
|
82
|
+
const result = await collection.updateOne(
|
|
83
|
+
{
|
|
84
|
+
feedUrl: data.url,
|
|
85
|
+
source: "microsub",
|
|
86
|
+
status: { $ne: "deleted" },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
$set: {
|
|
90
|
+
status: "deleted",
|
|
91
|
+
hidden: true,
|
|
92
|
+
deletedAt: now,
|
|
93
|
+
updatedAt: now,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (result.modifiedCount > 0) {
|
|
99
|
+
console.log(`[Microsub→Blogroll] Soft-deleted feed ${data.url}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extractDomain(url) {
|
|
105
|
+
try {
|
|
106
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
107
|
+
} catch {
|
|
108
|
+
return url;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function extractSiteUrl(feedUrl) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = new URL(feedUrl);
|
|
115
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
116
|
+
} catch {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -61,10 +61,10 @@ export async function processWebmention(application, source, target, userId) {
|
|
|
61
61
|
url: verification.url,
|
|
62
62
|
published: verification.published
|
|
63
63
|
? new Date(verification.published)
|
|
64
|
-
: new Date(),
|
|
64
|
+
: new Date(), // Keep as Date for query compatibility
|
|
65
65
|
verified: true,
|
|
66
66
|
readBy: [],
|
|
67
|
-
updatedAt: new Date(),
|
|
67
|
+
updatedAt: new Date().toISOString(),
|
|
68
68
|
};
|
|
69
69
|
|
|
70
70
|
if (existing) {
|
|
@@ -73,7 +73,7 @@ export async function processWebmention(application, source, target, userId) {
|
|
|
73
73
|
notification._id = existing._id;
|
|
74
74
|
} else {
|
|
75
75
|
// Insert new notification
|
|
76
|
-
notification.createdAt = new Date();
|
|
76
|
+
notification.createdAt = new Date().toISOString();
|
|
77
77
|
await collection.insertOne(notification);
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -190,7 +190,7 @@ function transformNotification(notification, userId) {
|
|
|
190
190
|
type: "entry",
|
|
191
191
|
uid: notification._id?.toString(),
|
|
192
192
|
url: notification.url || notification.source,
|
|
193
|
-
published: notification.published?.toISOString(),
|
|
193
|
+
published: notification.published?.toISOString(), // Convert Date to ISO string
|
|
194
194
|
author: notification.author,
|
|
195
195
|
content: notification.content,
|
|
196
196
|
_source: notification.source,
|
package/package.json
CHANGED