@rmdes/indiekit-endpoint-microsub 1.0.25 → 1.0.27
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/follow.js +17 -0
- package/lib/storage/channels.js +9 -1
- package/lib/storage/items.js +8 -0
- package/lib/utils/blogroll-notify.js +119 -0
- package/package.json +1 -1
|
@@ -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/storage/channels.js
CHANGED
|
@@ -83,6 +83,9 @@ export async function createChannel(application, { name, userId }) {
|
|
|
83
83
|
return channel;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// Retention period for unread count (only count recent items)
|
|
87
|
+
const UNREAD_RETENTION_DAYS = 30;
|
|
88
|
+
|
|
86
89
|
/**
|
|
87
90
|
* Get all channels for a user
|
|
88
91
|
* @param {object} application - Indiekit application
|
|
@@ -101,12 +104,17 @@ export async function getChannels(application, userId) {
|
|
|
101
104
|
.sort({ order: 1 })
|
|
102
105
|
.toArray();
|
|
103
106
|
|
|
104
|
-
//
|
|
107
|
+
// Calculate cutoff date for unread counts (only count recent items)
|
|
108
|
+
const cutoffDate = new Date();
|
|
109
|
+
cutoffDate.setDate(cutoffDate.getDate() - UNREAD_RETENTION_DAYS);
|
|
110
|
+
|
|
111
|
+
// Get unread counts for each channel (only recent items)
|
|
105
112
|
const channelsWithCounts = await Promise.all(
|
|
106
113
|
channels.map(async (channel) => {
|
|
107
114
|
const unreadCount = await itemsCollection.countDocuments({
|
|
108
115
|
channelId: channel._id,
|
|
109
116
|
readBy: { $ne: userId },
|
|
117
|
+
published: { $gte: cutoffDate },
|
|
110
118
|
});
|
|
111
119
|
|
|
112
120
|
return {
|
package/lib/storage/items.js
CHANGED
|
@@ -562,6 +562,9 @@ export async function deleteItemsForFeed(application, feedId) {
|
|
|
562
562
|
return result.deletedCount;
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
+
// Retention period for unread count (only count recent items)
|
|
566
|
+
const UNREAD_RETENTION_DAYS = 30;
|
|
567
|
+
|
|
565
568
|
/**
|
|
566
569
|
* Get unread count for a channel
|
|
567
570
|
* @param {object} application - Indiekit application
|
|
@@ -574,9 +577,14 @@ export async function getUnreadCount(application, channelId, userId) {
|
|
|
574
577
|
const objectId =
|
|
575
578
|
typeof channelId === "string" ? new ObjectId(channelId) : channelId;
|
|
576
579
|
|
|
580
|
+
// Only count items from the last UNREAD_RETENTION_DAYS
|
|
581
|
+
const cutoffDate = new Date();
|
|
582
|
+
cutoffDate.setDate(cutoffDate.getDate() - UNREAD_RETENTION_DAYS);
|
|
583
|
+
|
|
577
584
|
return collection.countDocuments({
|
|
578
585
|
channelId: objectId,
|
|
579
586
|
readBy: { $ne: userId },
|
|
587
|
+
published: { $gte: cutoffDate },
|
|
580
588
|
});
|
|
581
589
|
}
|
|
582
590
|
|
|
@@ -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
|
+
}
|
package/package.json
CHANGED