@rmdes/indiekit-endpoint-microsub 1.0.0-beta.11 → 1.0.0-beta.13
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/index.js +4 -0
- package/lib/controllers/block.js +4 -3
- package/lib/controllers/channels.js +4 -3
- package/lib/controllers/events.js +2 -1
- package/lib/controllers/follow.js +4 -3
- package/lib/controllers/mute.js +4 -3
- package/lib/controllers/reader.js +41 -12
- package/lib/controllers/search.js +2 -1
- package/lib/controllers/timeline.js +3 -2
- package/lib/storage/channels.js +17 -2
- package/lib/storage/items.js +62 -27
- package/lib/utils/auth.js +37 -0
- package/lib/webmention/receiver.js +2 -1
- package/locales/en.json +4 -0
- package/package.json +1 -1
- package/views/settings.njk +14 -0
package/index.js
CHANGED
|
@@ -78,6 +78,10 @@ export default class MicrosubEndpoint {
|
|
|
78
78
|
"/channels/:uid/settings",
|
|
79
79
|
readerController.updateSettings,
|
|
80
80
|
);
|
|
81
|
+
readerRouter.post(
|
|
82
|
+
"/channels/:uid/delete",
|
|
83
|
+
readerController.deleteChannel,
|
|
84
|
+
);
|
|
81
85
|
readerRouter.get("/channels/:uid/feeds", readerController.feeds);
|
|
82
86
|
readerRouter.post("/channels/:uid/feeds", readerController.addFeed);
|
|
83
87
|
readerRouter.post(
|
package/lib/controllers/block.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { deleteItemsByAuthorUrl } from "../storage/items.js";
|
|
7
|
+
import { getUserId } from "../utils/auth.js";
|
|
7
8
|
import { validateUrl } from "../utils/validation.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -23,7 +24,7 @@ function getCollection(application) {
|
|
|
23
24
|
*/
|
|
24
25
|
export async function list(request, response) {
|
|
25
26
|
const { application } = request.app.locals;
|
|
26
|
-
const userId = request
|
|
27
|
+
const userId = getUserId(request);
|
|
27
28
|
|
|
28
29
|
const collection = getCollection(application);
|
|
29
30
|
const blocked = await collection.find({ userId }).toArray();
|
|
@@ -40,7 +41,7 @@ export async function list(request, response) {
|
|
|
40
41
|
*/
|
|
41
42
|
export async function block(request, response) {
|
|
42
43
|
const { application } = request.app.locals;
|
|
43
|
-
const userId = request
|
|
44
|
+
const userId = getUserId(request);
|
|
44
45
|
const { url } = request.body;
|
|
45
46
|
|
|
46
47
|
validateUrl(url);
|
|
@@ -71,7 +72,7 @@ export async function block(request, response) {
|
|
|
71
72
|
*/
|
|
72
73
|
export async function unblock(request, response) {
|
|
73
74
|
const { application } = request.app.locals;
|
|
74
|
-
const userId = request
|
|
75
|
+
const userId = getUserId(request);
|
|
75
76
|
const { url } = request.body;
|
|
76
77
|
|
|
77
78
|
validateUrl(url);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
deleteChannel,
|
|
14
14
|
reorderChannels,
|
|
15
15
|
} from "../storage/channels.js";
|
|
16
|
+
import { getUserId } from "../utils/auth.js";
|
|
16
17
|
import {
|
|
17
18
|
validateChannel,
|
|
18
19
|
validateChannelName,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
*/
|
|
28
29
|
export async function list(request, response) {
|
|
29
30
|
const { application } = request.app.locals;
|
|
30
|
-
const userId = request
|
|
31
|
+
const userId = getUserId(request);
|
|
31
32
|
|
|
32
33
|
const channels = await getChannels(application, userId);
|
|
33
34
|
|
|
@@ -42,7 +43,7 @@ export async function list(request, response) {
|
|
|
42
43
|
*/
|
|
43
44
|
export async function action(request, response) {
|
|
44
45
|
const { application } = request.app.locals;
|
|
45
|
-
const userId = request
|
|
46
|
+
const userId = getUserId(request);
|
|
46
47
|
const { method, name, uid } = request.body;
|
|
47
48
|
|
|
48
49
|
// Delete channel
|
|
@@ -113,7 +114,7 @@ export async function action(request, response) {
|
|
|
113
114
|
*/
|
|
114
115
|
export async function get(request, response) {
|
|
115
116
|
const { application } = request.app.locals;
|
|
116
|
-
const userId = request
|
|
117
|
+
const userId = getUserId(request);
|
|
117
118
|
const { uid } = request.params;
|
|
118
119
|
|
|
119
120
|
validateChannel(uid);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
sendEvent,
|
|
10
10
|
subscribeClient,
|
|
11
11
|
} from "../realtime/broker.js";
|
|
12
|
+
import { getUserId } from "../utils/auth.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* SSE stream endpoint
|
|
@@ -18,7 +19,7 @@ import {
|
|
|
18
19
|
*/
|
|
19
20
|
export async function stream(request, response) {
|
|
20
21
|
const { application } = request.app.locals;
|
|
21
|
-
const userId = request
|
|
22
|
+
const userId = getUserId(request);
|
|
22
23
|
|
|
23
24
|
// Set SSE headers
|
|
24
25
|
response.setHeader("Content-Type", "text/event-stream");
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getFeedsForChannel,
|
|
14
14
|
} from "../storage/feeds.js";
|
|
15
15
|
import { createFeedResponse } from "../utils/jf2.js";
|
|
16
|
+
import { getUserId } from "../utils/auth.js";
|
|
16
17
|
import { validateChannel, validateUrl } from "../utils/validation.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -23,7 +24,7 @@ import { validateChannel, validateUrl } from "../utils/validation.js";
|
|
|
23
24
|
*/
|
|
24
25
|
export async function list(request, response) {
|
|
25
26
|
const { application } = request.app.locals;
|
|
26
|
-
const userId = request
|
|
27
|
+
const userId = getUserId(request);
|
|
27
28
|
const { channel } = request.query;
|
|
28
29
|
|
|
29
30
|
validateChannel(channel);
|
|
@@ -47,7 +48,7 @@ export async function list(request, response) {
|
|
|
47
48
|
*/
|
|
48
49
|
export async function follow(request, response) {
|
|
49
50
|
const { application } = request.app.locals;
|
|
50
|
-
const userId = request
|
|
51
|
+
const userId = getUserId(request);
|
|
51
52
|
const { channel, url } = request.body;
|
|
52
53
|
|
|
53
54
|
validateChannel(channel);
|
|
@@ -84,7 +85,7 @@ export async function follow(request, response) {
|
|
|
84
85
|
*/
|
|
85
86
|
export async function unfollow(request, response) {
|
|
86
87
|
const { application } = request.app.locals;
|
|
87
|
-
const userId = request
|
|
88
|
+
const userId = getUserId(request);
|
|
88
89
|
const { channel, url } = request.body;
|
|
89
90
|
|
|
90
91
|
validateChannel(channel);
|
package/lib/controllers/mute.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { IndiekitError } from "@indiekit/error";
|
|
7
7
|
|
|
8
|
+
import { getUserId } from "../utils/auth.js";
|
|
8
9
|
import { validateChannel, validateUrl } from "../utils/validation.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -24,7 +25,7 @@ function getCollection(application) {
|
|
|
24
25
|
*/
|
|
25
26
|
export async function list(request, response) {
|
|
26
27
|
const { application } = request.app.locals;
|
|
27
|
-
const userId = request
|
|
28
|
+
const userId = getUserId(request);
|
|
28
29
|
const { channel } = request.query;
|
|
29
30
|
|
|
30
31
|
// Channel can be "global" or a specific channel UID
|
|
@@ -58,7 +59,7 @@ export async function list(request, response) {
|
|
|
58
59
|
*/
|
|
59
60
|
export async function mute(request, response) {
|
|
60
61
|
const { application } = request.app.locals;
|
|
61
|
-
const userId = request
|
|
62
|
+
const userId = getUserId(request);
|
|
62
63
|
const { channel, url } = request.body;
|
|
63
64
|
|
|
64
65
|
validateUrl(url);
|
|
@@ -99,7 +100,7 @@ export async function mute(request, response) {
|
|
|
99
100
|
*/
|
|
100
101
|
export async function unmute(request, response) {
|
|
101
102
|
const { application } = request.app.locals;
|
|
102
|
-
const userId = request
|
|
103
|
+
const userId = getUserId(request);
|
|
103
104
|
const { channel, url } = request.body;
|
|
104
105
|
|
|
105
106
|
validateUrl(url);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getChannel,
|
|
11
11
|
createChannel,
|
|
12
12
|
updateChannelSettings,
|
|
13
|
+
deleteChannel,
|
|
13
14
|
} from "../storage/channels.js";
|
|
14
15
|
import {
|
|
15
16
|
getFeedsForChannel,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
deleteFeed,
|
|
18
19
|
} from "../storage/feeds.js";
|
|
19
20
|
import { getTimelineItems, getItemById } from "../storage/items.js";
|
|
21
|
+
import { getUserId } from "../utils/auth.js";
|
|
20
22
|
import {
|
|
21
23
|
validateChannelName,
|
|
22
24
|
validateExcludeTypes,
|
|
@@ -39,7 +41,7 @@ export async function index(request, response) {
|
|
|
39
41
|
*/
|
|
40
42
|
export async function channels(request, response) {
|
|
41
43
|
const { application } = request.app.locals;
|
|
42
|
-
const userId = request
|
|
44
|
+
const userId = getUserId(request);
|
|
43
45
|
|
|
44
46
|
const channelList = await getChannels(application, userId);
|
|
45
47
|
|
|
@@ -69,7 +71,7 @@ export async function newChannel(request, response) {
|
|
|
69
71
|
*/
|
|
70
72
|
export async function createChannelAction(request, response) {
|
|
71
73
|
const { application } = request.app.locals;
|
|
72
|
-
const userId = request
|
|
74
|
+
const userId = getUserId(request);
|
|
73
75
|
const { name } = request.body;
|
|
74
76
|
|
|
75
77
|
validateChannelName(name);
|
|
@@ -87,7 +89,7 @@ export async function createChannelAction(request, response) {
|
|
|
87
89
|
*/
|
|
88
90
|
export async function channel(request, response) {
|
|
89
91
|
const { application } = request.app.locals;
|
|
90
|
-
const userId = request
|
|
92
|
+
const userId = getUserId(request);
|
|
91
93
|
const { uid } = request.params;
|
|
92
94
|
const { before, after } = request.query;
|
|
93
95
|
|
|
@@ -119,7 +121,7 @@ export async function channel(request, response) {
|
|
|
119
121
|
*/
|
|
120
122
|
export async function settings(request, response) {
|
|
121
123
|
const { application } = request.app.locals;
|
|
122
|
-
const userId = request
|
|
124
|
+
const userId = getUserId(request);
|
|
123
125
|
const { uid } = request.params;
|
|
124
126
|
|
|
125
127
|
const channelDocument = await getChannel(application, uid, userId);
|
|
@@ -144,7 +146,7 @@ export async function settings(request, response) {
|
|
|
144
146
|
*/
|
|
145
147
|
export async function updateSettings(request, response) {
|
|
146
148
|
const { application } = request.app.locals;
|
|
147
|
-
const userId = request
|
|
149
|
+
const userId = getUserId(request);
|
|
148
150
|
const { uid } = request.params;
|
|
149
151
|
const { excludeTypes, excludeRegex } = request.body;
|
|
150
152
|
|
|
@@ -171,6 +173,32 @@ export async function updateSettings(request, response) {
|
|
|
171
173
|
response.redirect(`${request.baseUrl}/channels/${uid}`);
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Delete channel
|
|
178
|
+
* @param {object} request - Express request
|
|
179
|
+
* @param {object} response - Express response
|
|
180
|
+
* @returns {Promise<void>}
|
|
181
|
+
*/
|
|
182
|
+
export async function deleteChannelAction(request, response) {
|
|
183
|
+
const { application } = request.app.locals;
|
|
184
|
+
const userId = getUserId(request);
|
|
185
|
+
const { uid } = request.params;
|
|
186
|
+
|
|
187
|
+
// Don't allow deleting notifications channel
|
|
188
|
+
if (uid === "notifications") {
|
|
189
|
+
return response.redirect(`${request.baseUrl}/channels`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
193
|
+
if (!channelDocument) {
|
|
194
|
+
return response.status(404).render("404");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await deleteChannel(application, uid, userId);
|
|
198
|
+
|
|
199
|
+
response.redirect(`${request.baseUrl}/channels`);
|
|
200
|
+
}
|
|
201
|
+
|
|
174
202
|
/**
|
|
175
203
|
* View feeds for a channel
|
|
176
204
|
* @param {object} request - Express request
|
|
@@ -179,7 +207,7 @@ export async function updateSettings(request, response) {
|
|
|
179
207
|
*/
|
|
180
208
|
export async function feeds(request, response) {
|
|
181
209
|
const { application } = request.app.locals;
|
|
182
|
-
const userId = request
|
|
210
|
+
const userId = getUserId(request);
|
|
183
211
|
const { uid } = request.params;
|
|
184
212
|
|
|
185
213
|
const channelDocument = await getChannel(application, uid, userId);
|
|
@@ -205,7 +233,7 @@ export async function feeds(request, response) {
|
|
|
205
233
|
*/
|
|
206
234
|
export async function addFeed(request, response) {
|
|
207
235
|
const { application } = request.app.locals;
|
|
208
|
-
const userId = request
|
|
236
|
+
const userId = getUserId(request);
|
|
209
237
|
const { uid } = request.params;
|
|
210
238
|
const { url } = request.body;
|
|
211
239
|
|
|
@@ -238,7 +266,7 @@ export async function addFeed(request, response) {
|
|
|
238
266
|
*/
|
|
239
267
|
export async function removeFeed(request, response) {
|
|
240
268
|
const { application } = request.app.locals;
|
|
241
|
-
const userId = request
|
|
269
|
+
const userId = getUserId(request);
|
|
242
270
|
const { uid } = request.params;
|
|
243
271
|
const { url } = request.body;
|
|
244
272
|
|
|
@@ -260,7 +288,7 @@ export async function removeFeed(request, response) {
|
|
|
260
288
|
*/
|
|
261
289
|
export async function item(request, response) {
|
|
262
290
|
const { application } = request.app.locals;
|
|
263
|
-
const userId = request
|
|
291
|
+
const userId = getUserId(request);
|
|
264
292
|
const { id } = request.params;
|
|
265
293
|
|
|
266
294
|
const itemDocument = await getItemById(application, id, userId);
|
|
@@ -311,7 +339,7 @@ export async function submitCompose(request, response) {
|
|
|
311
339
|
*/
|
|
312
340
|
export async function searchPage(request, response) {
|
|
313
341
|
const { application } = request.app.locals;
|
|
314
|
-
const userId = request
|
|
342
|
+
const userId = getUserId(request);
|
|
315
343
|
|
|
316
344
|
const channelList = await getChannels(application, userId);
|
|
317
345
|
|
|
@@ -330,7 +358,7 @@ export async function searchPage(request, response) {
|
|
|
330
358
|
*/
|
|
331
359
|
export async function searchFeeds(request, response) {
|
|
332
360
|
const { application } = request.app.locals;
|
|
333
|
-
const userId = request
|
|
361
|
+
const userId = getUserId(request);
|
|
334
362
|
const { query } = request.body;
|
|
335
363
|
|
|
336
364
|
const channelList = await getChannels(application, userId);
|
|
@@ -362,7 +390,7 @@ export async function searchFeeds(request, response) {
|
|
|
362
390
|
*/
|
|
363
391
|
export async function subscribe(request, response) {
|
|
364
392
|
const { application } = request.app.locals;
|
|
365
|
-
const userId = request
|
|
393
|
+
const userId = getUserId(request);
|
|
366
394
|
const { url, channel: channelUid } = request.body;
|
|
367
395
|
|
|
368
396
|
const channelDocument = await getChannel(application, channelUid, userId);
|
|
@@ -394,6 +422,7 @@ export const readerController = {
|
|
|
394
422
|
channel,
|
|
395
423
|
settings,
|
|
396
424
|
updateSettings,
|
|
425
|
+
deleteChannel: deleteChannelAction,
|
|
397
426
|
feeds,
|
|
398
427
|
addFeed,
|
|
399
428
|
removeFeed,
|
|
@@ -8,6 +8,7 @@ import { IndiekitError } from "@indiekit/error";
|
|
|
8
8
|
import { discoverFeeds } from "../feeds/hfeed.js";
|
|
9
9
|
import { searchWithFallback } from "../search/query.js";
|
|
10
10
|
import { getChannel } from "../storage/channels.js";
|
|
11
|
+
import { getUserId } from "../utils/auth.js";
|
|
11
12
|
import { validateChannel, validateUrl } from "../utils/validation.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -77,7 +78,7 @@ export async function discover(request, response) {
|
|
|
77
78
|
*/
|
|
78
79
|
export async function search(request, response) {
|
|
79
80
|
const { application } = request.app.locals;
|
|
80
|
-
const userId = request
|
|
81
|
+
const userId = getUserId(request);
|
|
81
82
|
const { query, channel } = request.body;
|
|
82
83
|
|
|
83
84
|
if (!query) {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
markItemsUnread,
|
|
13
13
|
removeItems,
|
|
14
14
|
} from "../storage/items.js";
|
|
15
|
+
import { getUserId } from "../utils/auth.js";
|
|
15
16
|
import {
|
|
16
17
|
validateChannel,
|
|
17
18
|
validateEntries,
|
|
@@ -26,7 +27,7 @@ import {
|
|
|
26
27
|
*/
|
|
27
28
|
export async function get(request, response) {
|
|
28
29
|
const { application } = request.app.locals;
|
|
29
|
-
const userId = request
|
|
30
|
+
const userId = getUserId(request);
|
|
30
31
|
const { channel, before, after, limit } = request.query;
|
|
31
32
|
|
|
32
33
|
validateChannel(channel);
|
|
@@ -57,7 +58,7 @@ export async function get(request, response) {
|
|
|
57
58
|
*/
|
|
58
59
|
export async function action(request, response) {
|
|
59
60
|
const { application } = request.app.locals;
|
|
60
|
-
const userId = request
|
|
61
|
+
const userId = getUserId(request);
|
|
61
62
|
const { method, channel } = request.body;
|
|
62
63
|
|
|
63
64
|
validateChannel(channel);
|
package/lib/storage/channels.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { ObjectId } from "mongodb";
|
|
7
7
|
|
|
8
|
+
import { deleteFeedsForChannel } from "./feeds.js";
|
|
9
|
+
import { deleteItemsForChannel } from "./items.js";
|
|
8
10
|
import { generateChannelUid } from "../utils/jf2.js";
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -184,7 +186,7 @@ export async function updateChannel(application, uid, updates, userId) {
|
|
|
184
186
|
}
|
|
185
187
|
|
|
186
188
|
/**
|
|
187
|
-
* Delete a channel
|
|
189
|
+
* Delete a channel and all its feeds and items
|
|
188
190
|
* @param {object} application - Indiekit application
|
|
189
191
|
* @param {string} uid - Channel UID
|
|
190
192
|
* @param {string} [userId] - User ID
|
|
@@ -200,7 +202,20 @@ export async function deleteChannel(application, uid, userId) {
|
|
|
200
202
|
return false;
|
|
201
203
|
}
|
|
202
204
|
|
|
203
|
-
|
|
205
|
+
// Find the channel first to get its ObjectId
|
|
206
|
+
const channel = await collection.findOne(query);
|
|
207
|
+
if (!channel) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Cascade delete: items first, then feeds, then channel
|
|
212
|
+
const itemsDeleted = await deleteItemsForChannel(application, channel._id);
|
|
213
|
+
const feedsDeleted = await deleteFeedsForChannel(application, channel._id);
|
|
214
|
+
console.info(
|
|
215
|
+
`[Microsub] Deleted channel ${uid}: ${feedsDeleted} feeds, ${itemsDeleted} items`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const result = await collection.deleteOne({ _id: channel._id });
|
|
204
219
|
return result.deletedCount > 0;
|
|
205
220
|
}
|
|
206
221
|
|
package/lib/storage/items.js
CHANGED
|
@@ -195,7 +195,7 @@ export async function getItemsByUids(application, uids, userId) {
|
|
|
195
195
|
* Mark items as read
|
|
196
196
|
* @param {object} application - Indiekit application
|
|
197
197
|
* @param {ObjectId|string} channelId - Channel ObjectId
|
|
198
|
-
* @param {Array} entryIds - Array of entry IDs to mark as read
|
|
198
|
+
* @param {Array} entryIds - Array of entry IDs to mark as read (can be ObjectId, uid, or URL)
|
|
199
199
|
* @param {string} userId - User ID
|
|
200
200
|
* @returns {Promise<number>} Number of items updated
|
|
201
201
|
*/
|
|
@@ -204,6 +204,12 @@ export async function markItemsRead(application, channelId, entryIds, userId) {
|
|
|
204
204
|
const channelObjectId =
|
|
205
205
|
typeof channelId === "string" ? new ObjectId(channelId) : channelId;
|
|
206
206
|
|
|
207
|
+
console.info(
|
|
208
|
+
`[Microsub] markItemsRead called for channel ${channelId}, entries:`,
|
|
209
|
+
entryIds,
|
|
210
|
+
`userId: ${userId}`,
|
|
211
|
+
);
|
|
212
|
+
|
|
207
213
|
// Handle "last-read-entry" special value
|
|
208
214
|
if (entryIds.includes("last-read-entry")) {
|
|
209
215
|
// Mark all items in channel as read
|
|
@@ -211,26 +217,39 @@ export async function markItemsRead(application, channelId, entryIds, userId) {
|
|
|
211
217
|
{ channelId: channelObjectId },
|
|
212
218
|
{ $addToSet: { readBy: userId } },
|
|
213
219
|
);
|
|
220
|
+
console.info(
|
|
221
|
+
`[Microsub] Marked all items as read: ${result.modifiedCount} updated`,
|
|
222
|
+
);
|
|
214
223
|
return result.modifiedCount;
|
|
215
224
|
}
|
|
216
225
|
|
|
217
226
|
// Convert string IDs to ObjectIds where possible
|
|
218
|
-
const objectIds = entryIds
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
227
|
+
const objectIds = entryIds
|
|
228
|
+
.map((id) => {
|
|
229
|
+
try {
|
|
230
|
+
return new ObjectId(id);
|
|
231
|
+
} catch {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
.filter(Boolean);
|
|
225
236
|
|
|
237
|
+
// Build query to match by _id, uid, or url (Microsub spec uses URLs as entry identifiers)
|
|
226
238
|
const result = await collection.updateMany(
|
|
227
239
|
{
|
|
228
240
|
channelId: channelObjectId,
|
|
229
|
-
$or: [
|
|
241
|
+
$or: [
|
|
242
|
+
...(objectIds.length > 0 ? [{ _id: { $in: objectIds } }] : []),
|
|
243
|
+
{ uid: { $in: entryIds } },
|
|
244
|
+
{ url: { $in: entryIds } },
|
|
245
|
+
],
|
|
230
246
|
},
|
|
231
247
|
{ $addToSet: { readBy: userId } },
|
|
232
248
|
);
|
|
233
249
|
|
|
250
|
+
console.info(
|
|
251
|
+
`[Microsub] markItemsRead result: ${result.modifiedCount} items updated`,
|
|
252
|
+
);
|
|
234
253
|
return result.modifiedCount;
|
|
235
254
|
}
|
|
236
255
|
|
|
@@ -238,7 +257,7 @@ export async function markItemsRead(application, channelId, entryIds, userId) {
|
|
|
238
257
|
* Mark items as unread
|
|
239
258
|
* @param {object} application - Indiekit application
|
|
240
259
|
* @param {ObjectId|string} channelId - Channel ObjectId
|
|
241
|
-
* @param {Array} entryIds - Array of entry IDs to mark as unread
|
|
260
|
+
* @param {Array} entryIds - Array of entry IDs to mark as unread (can be ObjectId, uid, or URL)
|
|
242
261
|
* @param {string} userId - User ID
|
|
243
262
|
* @returns {Promise<number>} Number of items updated
|
|
244
263
|
*/
|
|
@@ -252,18 +271,26 @@ export async function markItemsUnread(
|
|
|
252
271
|
const channelObjectId =
|
|
253
272
|
typeof channelId === "string" ? new ObjectId(channelId) : channelId;
|
|
254
273
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
// Convert string IDs to ObjectIds where possible
|
|
275
|
+
const objectIds = entryIds
|
|
276
|
+
.map((id) => {
|
|
277
|
+
try {
|
|
278
|
+
return new ObjectId(id);
|
|
279
|
+
} catch {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
.filter(Boolean);
|
|
262
284
|
|
|
285
|
+
// Match by _id, uid, or url
|
|
263
286
|
const result = await collection.updateMany(
|
|
264
287
|
{
|
|
265
288
|
channelId: channelObjectId,
|
|
266
|
-
$or: [
|
|
289
|
+
$or: [
|
|
290
|
+
...(objectIds.length > 0 ? [{ _id: { $in: objectIds } }] : []),
|
|
291
|
+
{ uid: { $in: entryIds } },
|
|
292
|
+
{ url: { $in: entryIds } },
|
|
293
|
+
],
|
|
267
294
|
},
|
|
268
295
|
{ $pull: { readBy: userId } },
|
|
269
296
|
);
|
|
@@ -275,7 +302,7 @@ export async function markItemsUnread(
|
|
|
275
302
|
* Remove items from channel
|
|
276
303
|
* @param {object} application - Indiekit application
|
|
277
304
|
* @param {ObjectId|string} channelId - Channel ObjectId
|
|
278
|
-
* @param {Array} entryIds - Array of entry IDs to remove
|
|
305
|
+
* @param {Array} entryIds - Array of entry IDs to remove (can be ObjectId, uid, or URL)
|
|
279
306
|
* @returns {Promise<number>} Number of items removed
|
|
280
307
|
*/
|
|
281
308
|
export async function removeItems(application, channelId, entryIds) {
|
|
@@ -283,17 +310,25 @@ export async function removeItems(application, channelId, entryIds) {
|
|
|
283
310
|
const channelObjectId =
|
|
284
311
|
typeof channelId === "string" ? new ObjectId(channelId) : channelId;
|
|
285
312
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
313
|
+
// Convert string IDs to ObjectIds where possible
|
|
314
|
+
const objectIds = entryIds
|
|
315
|
+
.map((id) => {
|
|
316
|
+
try {
|
|
317
|
+
return new ObjectId(id);
|
|
318
|
+
} catch {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
.filter(Boolean);
|
|
293
323
|
|
|
324
|
+
// Match by _id, uid, or url
|
|
294
325
|
const result = await collection.deleteMany({
|
|
295
326
|
channelId: channelObjectId,
|
|
296
|
-
$or: [
|
|
327
|
+
$or: [
|
|
328
|
+
...(objectIds.length > 0 ? [{ _id: { $in: objectIds } }] : []),
|
|
329
|
+
{ uid: { $in: entryIds } },
|
|
330
|
+
{ url: { $in: entryIds } },
|
|
331
|
+
],
|
|
297
332
|
});
|
|
298
333
|
|
|
299
334
|
return result.deletedCount;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication utilities for Microsub
|
|
3
|
+
* @module utils/auth
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the user ID from request context
|
|
8
|
+
*
|
|
9
|
+
* In Indiekit, the userId can come from:
|
|
10
|
+
* 1. request.session.userId (if explicitly set)
|
|
11
|
+
* 2. request.session.me (from token introspection)
|
|
12
|
+
* 3. application.publication.me (single-user fallback)
|
|
13
|
+
*
|
|
14
|
+
* @param {object} request - Express request
|
|
15
|
+
* @returns {string|undefined} User ID
|
|
16
|
+
*/
|
|
17
|
+
export function getUserId(request) {
|
|
18
|
+
// Check session for explicit userId
|
|
19
|
+
if (request.session?.userId) {
|
|
20
|
+
return request.session.userId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check session for me URL from token introspection
|
|
24
|
+
if (request.session?.me) {
|
|
25
|
+
return request.session.me;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Fall back to publication me URL (single-user mode)
|
|
29
|
+
const { application } = request.app.locals;
|
|
30
|
+
if (application?.publication?.me) {
|
|
31
|
+
return application.publication.me;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Final fallback: use "default" as user ID for single-user instances
|
|
35
|
+
// This ensures read state is tracked even without explicit user identity
|
|
36
|
+
return "default";
|
|
37
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @module webmention/receiver
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { getUserId } from "../utils/auth.js";
|
|
6
7
|
import { processWebmention } from "./processor.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -33,7 +34,7 @@ export async function receive(request, response) {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const { application } = request.app.locals;
|
|
36
|
-
const userId = request
|
|
37
|
+
const userId = getUserId(request);
|
|
37
38
|
|
|
38
39
|
// Return 202 Accepted immediately (processing asynchronously)
|
|
39
40
|
response.status(202).json({
|
package/locales/en.json
CHANGED
|
@@ -55,6 +55,10 @@
|
|
|
55
55
|
"excludeRegex": "Exclude pattern",
|
|
56
56
|
"excludeRegexHelp": "Regular expression to filter out matching content",
|
|
57
57
|
"save": "Save settings",
|
|
58
|
+
"dangerZone": "Danger zone",
|
|
59
|
+
"deleteWarning": "Deleting this channel will permanently remove all feeds and items. This action cannot be undone.",
|
|
60
|
+
"deleteConfirm": "Are you sure you want to delete this channel and all its content?",
|
|
61
|
+
"delete": "Delete channel",
|
|
58
62
|
"types": {
|
|
59
63
|
"like": "Likes",
|
|
60
64
|
"repost": "Reposts",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-microsub",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.13",
|
|
4
4
|
"description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"indiekit",
|
package/views/settings.njk
CHANGED
|
@@ -55,5 +55,19 @@
|
|
|
55
55
|
</a>
|
|
56
56
|
</div>
|
|
57
57
|
</form>
|
|
58
|
+
|
|
59
|
+
{% if channel.uid !== "notifications" %}
|
|
60
|
+
<hr class="divider">
|
|
61
|
+
<div class="danger-zone">
|
|
62
|
+
<h3>{{ __("microsub.settings.dangerZone") }}</h3>
|
|
63
|
+
<p class="hint">{{ __("microsub.settings.deleteWarning") }}</p>
|
|
64
|
+
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/delete" onsubmit="return confirm('{{ __("microsub.settings.deleteConfirm") }}');">
|
|
65
|
+
{{ button({
|
|
66
|
+
text: __("microsub.settings.delete"),
|
|
67
|
+
classes: "button--danger"
|
|
68
|
+
}) }}
|
|
69
|
+
</form>
|
|
70
|
+
</div>
|
|
71
|
+
{% endif %}
|
|
58
72
|
</div>
|
|
59
73
|
{% endblock %}
|