@rmdes/indiekit-endpoint-microsub 1.0.11 → 1.0.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/assets/styles.css +0 -37
- package/index.js +0 -1
- package/lib/controllers/reader.js +7 -110
- package/lib/storage/items.js +0 -24
- package/locales/en.json +1 -8
- package/package.json +4 -8
- package/views/channel.njk +0 -19
- package/views/compose.njk +2 -26
- package/README.md +0 -111
package/assets/styles.css
CHANGED
|
@@ -679,43 +679,6 @@
|
|
|
679
679
|
text-align: right;
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
-
.compose__syndication {
|
|
683
|
-
background: var(--color-offset);
|
|
684
|
-
border: none;
|
|
685
|
-
border-radius: var(--border-radius);
|
|
686
|
-
margin: var(--space-m) 0;
|
|
687
|
-
padding: var(--space-m);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
.compose__syndication legend {
|
|
691
|
-
font-weight: 600;
|
|
692
|
-
margin-bottom: var(--space-xs);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
.compose__syndication .hint {
|
|
696
|
-
color: var(--color-text-muted);
|
|
697
|
-
font-size: var(--font-size-small);
|
|
698
|
-
margin-bottom: var(--space-s);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
.syndication-target {
|
|
702
|
-
align-items: center;
|
|
703
|
-
cursor: pointer;
|
|
704
|
-
display: flex;
|
|
705
|
-
gap: var(--space-s);
|
|
706
|
-
padding: var(--space-xs) 0;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
.syndication-target input[type="checkbox"] {
|
|
710
|
-
flex-shrink: 0;
|
|
711
|
-
height: 1.25rem;
|
|
712
|
-
width: 1.25rem;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
.syndication-target__name {
|
|
716
|
-
flex: 1;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
682
|
/* ==========================================================================
|
|
720
683
|
Settings
|
|
721
684
|
========================================================================== */
|
package/index.js
CHANGED
|
@@ -96,7 +96,6 @@ export default class MicrosubEndpoint {
|
|
|
96
96
|
readerRouter.get("/search", readerController.searchPage);
|
|
97
97
|
readerRouter.post("/search", readerController.searchFeeds);
|
|
98
98
|
readerRouter.post("/subscribe", readerController.subscribe);
|
|
99
|
-
readerRouter.post("/api/mark-read", readerController.markAllRead);
|
|
100
99
|
router.use("/reader", readerRouter);
|
|
101
100
|
|
|
102
101
|
return router;
|
|
@@ -17,12 +17,7 @@ import {
|
|
|
17
17
|
createFeed,
|
|
18
18
|
deleteFeed,
|
|
19
19
|
} from "../storage/feeds.js";
|
|
20
|
-
import {
|
|
21
|
-
getTimelineItems,
|
|
22
|
-
getItemById,
|
|
23
|
-
markItemsRead,
|
|
24
|
-
countReadItems,
|
|
25
|
-
} from "../storage/items.js";
|
|
20
|
+
import { getTimelineItems, getItemById } from "../storage/items.js";
|
|
26
21
|
import { getUserId } from "../utils/auth.js";
|
|
27
22
|
import {
|
|
28
23
|
validateChannelName,
|
|
@@ -96,37 +91,24 @@ export async function channel(request, response) {
|
|
|
96
91
|
const { application } = request.app.locals;
|
|
97
92
|
const userId = getUserId(request);
|
|
98
93
|
const { uid } = request.params;
|
|
99
|
-
const { before, after
|
|
94
|
+
const { before, after } = request.query;
|
|
100
95
|
|
|
101
96
|
const channelDocument = await getChannel(application, uid, userId);
|
|
102
97
|
if (!channelDocument) {
|
|
103
98
|
return response.status(404).render("404");
|
|
104
99
|
}
|
|
105
100
|
|
|
106
|
-
// Check if showing read items
|
|
107
|
-
const showReadItems = showRead === "true";
|
|
108
|
-
|
|
109
101
|
const timeline = await getTimelineItems(application, channelDocument._id, {
|
|
110
102
|
before,
|
|
111
103
|
after,
|
|
112
104
|
userId,
|
|
113
|
-
showRead: showReadItems,
|
|
114
105
|
});
|
|
115
106
|
|
|
116
|
-
// Count read items to show "View read items" button
|
|
117
|
-
const readCount = await countReadItems(
|
|
118
|
-
application,
|
|
119
|
-
channelDocument._id,
|
|
120
|
-
userId,
|
|
121
|
-
);
|
|
122
|
-
|
|
123
107
|
response.render("channel", {
|
|
124
108
|
title: channelDocument.name,
|
|
125
109
|
channel: channelDocument,
|
|
126
110
|
items: timeline.items,
|
|
127
111
|
paging: timeline.paging,
|
|
128
|
-
readCount,
|
|
129
|
-
showRead: showReadItems,
|
|
130
112
|
baseUrl: request.baseUrl,
|
|
131
113
|
});
|
|
132
114
|
}
|
|
@@ -191,33 +173,6 @@ export async function updateSettings(request, response) {
|
|
|
191
173
|
response.redirect(`${request.baseUrl}/channels/${uid}`);
|
|
192
174
|
}
|
|
193
175
|
|
|
194
|
-
/**
|
|
195
|
-
* Mark all items in channel as read
|
|
196
|
-
* @param {object} request - Express request
|
|
197
|
-
* @param {object} response - Express response
|
|
198
|
-
* @returns {Promise<void>}
|
|
199
|
-
*/
|
|
200
|
-
export async function markAllRead(request, response) {
|
|
201
|
-
const { application } = request.app.locals;
|
|
202
|
-
const userId = getUserId(request);
|
|
203
|
-
const { channel: channelUid } = request.body;
|
|
204
|
-
|
|
205
|
-
const channelDocument = await getChannel(application, channelUid, userId);
|
|
206
|
-
if (!channelDocument) {
|
|
207
|
-
return response.status(404).render("404");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Mark all items as read using the special "last-read-entry" value
|
|
211
|
-
await markItemsRead(
|
|
212
|
-
application,
|
|
213
|
-
channelDocument._id,
|
|
214
|
-
["last-read-entry"],
|
|
215
|
-
userId,
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
response.redirect(`${request.baseUrl}/channels/${channelUid}`);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
176
|
/**
|
|
222
177
|
* Delete channel
|
|
223
178
|
* @param {object} request - Express request
|
|
@@ -360,38 +315,6 @@ function ensureString(value) {
|
|
|
360
315
|
return String(value);
|
|
361
316
|
}
|
|
362
317
|
|
|
363
|
-
/**
|
|
364
|
-
* Fetch syndication targets from Micropub config
|
|
365
|
-
* @param {object} application - Indiekit application
|
|
366
|
-
* @param {string} token - Auth token
|
|
367
|
-
* @returns {Promise<Array>} Syndication targets
|
|
368
|
-
*/
|
|
369
|
-
async function getSyndicationTargets(application, token) {
|
|
370
|
-
try {
|
|
371
|
-
const micropubEndpoint = application.micropubEndpoint;
|
|
372
|
-
if (!micropubEndpoint) return [];
|
|
373
|
-
|
|
374
|
-
const micropubUrl = micropubEndpoint.startsWith("http")
|
|
375
|
-
? micropubEndpoint
|
|
376
|
-
: new URL(micropubEndpoint, application.url).href;
|
|
377
|
-
|
|
378
|
-
const configUrl = `${micropubUrl}?q=config`;
|
|
379
|
-
const configResponse = await fetch(configUrl, {
|
|
380
|
-
headers: {
|
|
381
|
-
Authorization: `Bearer ${token}`,
|
|
382
|
-
Accept: "application/json",
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
if (!configResponse.ok) return [];
|
|
387
|
-
|
|
388
|
-
const config = await configResponse.json();
|
|
389
|
-
return config["syndicate-to"] || [];
|
|
390
|
-
} catch {
|
|
391
|
-
return [];
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
318
|
/**
|
|
396
319
|
* Compose response form
|
|
397
320
|
* @param {object} request - Express request
|
|
@@ -399,8 +322,6 @@ async function getSyndicationTargets(application, token) {
|
|
|
399
322
|
* @returns {Promise<void>}
|
|
400
323
|
*/
|
|
401
324
|
export async function compose(request, response) {
|
|
402
|
-
const { application } = request.app.locals;
|
|
403
|
-
|
|
404
325
|
// Support both long-form (replyTo) and short-form (reply) query params
|
|
405
326
|
const {
|
|
406
327
|
replyTo,
|
|
@@ -413,19 +334,12 @@ export async function compose(request, response) {
|
|
|
413
334
|
bookmark,
|
|
414
335
|
} = request.query;
|
|
415
336
|
|
|
416
|
-
// Fetch syndication targets if user is authenticated
|
|
417
|
-
const token = request.session?.access_token;
|
|
418
|
-
const syndicationTargets = token
|
|
419
|
-
? await getSyndicationTargets(application, token)
|
|
420
|
-
: [];
|
|
421
|
-
|
|
422
337
|
response.render("compose", {
|
|
423
338
|
title: request.__("microsub.compose.title"),
|
|
424
339
|
replyTo: ensureString(replyTo || reply),
|
|
425
340
|
likeOf: ensureString(likeOf || like),
|
|
426
341
|
repostOf: ensureString(repostOf || repost),
|
|
427
342
|
bookmarkOf: ensureString(bookmarkOf || bookmark),
|
|
428
|
-
syndicationTargets,
|
|
429
343
|
baseUrl: request.baseUrl,
|
|
430
344
|
});
|
|
431
345
|
}
|
|
@@ -443,7 +357,6 @@ export async function submitCompose(request, response) {
|
|
|
443
357
|
const likeOf = request.body["like-of"];
|
|
444
358
|
const repostOf = request.body["repost-of"];
|
|
445
359
|
const bookmarkOf = request.body["bookmark-of"];
|
|
446
|
-
const syndicateTo = request.body["mp-syndicate-to"];
|
|
447
360
|
|
|
448
361
|
// Debug logging
|
|
449
362
|
console.info(
|
|
@@ -456,7 +369,6 @@ export async function submitCompose(request, response) {
|
|
|
456
369
|
likeOf,
|
|
457
370
|
repostOf,
|
|
458
371
|
bookmarkOf,
|
|
459
|
-
syndicateTo,
|
|
460
372
|
});
|
|
461
373
|
|
|
462
374
|
// Get Micropub endpoint
|
|
@@ -484,22 +396,16 @@ export async function submitCompose(request, response) {
|
|
|
484
396
|
micropubData.append("h", "entry");
|
|
485
397
|
|
|
486
398
|
if (likeOf) {
|
|
487
|
-
// Like post
|
|
399
|
+
// Like post (no content needed)
|
|
488
400
|
micropubData.append("like-of", likeOf);
|
|
489
|
-
if (content && content.trim()) {
|
|
490
|
-
micropubData.append("content", content.trim());
|
|
491
|
-
}
|
|
492
401
|
} else if (repostOf) {
|
|
493
|
-
// Repost
|
|
402
|
+
// Repost (no content needed)
|
|
494
403
|
micropubData.append("repost-of", repostOf);
|
|
495
|
-
if (content && content.trim()) {
|
|
496
|
-
micropubData.append("content", content.trim());
|
|
497
|
-
}
|
|
498
404
|
} else if (bookmarkOf) {
|
|
499
|
-
// Bookmark
|
|
405
|
+
// Bookmark (content optional)
|
|
500
406
|
micropubData.append("bookmark-of", bookmarkOf);
|
|
501
|
-
if (content
|
|
502
|
-
micropubData.append("content", content
|
|
407
|
+
if (content) {
|
|
408
|
+
micropubData.append("content", content);
|
|
503
409
|
}
|
|
504
410
|
} else if (inReplyTo) {
|
|
505
411
|
// Reply
|
|
@@ -510,14 +416,6 @@ export async function submitCompose(request, response) {
|
|
|
510
416
|
micropubData.append("content", content || "");
|
|
511
417
|
}
|
|
512
418
|
|
|
513
|
-
// Add syndication targets
|
|
514
|
-
if (syndicateTo) {
|
|
515
|
-
const targets = Array.isArray(syndicateTo) ? syndicateTo : [syndicateTo];
|
|
516
|
-
for (const target of targets) {
|
|
517
|
-
micropubData.append("mp-syndicate-to", target);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
419
|
// Debug: log what we're sending
|
|
522
420
|
console.info("[Microsub] Sending to Micropub:", {
|
|
523
421
|
url: micropubUrl,
|
|
@@ -675,7 +573,6 @@ export const readerController = {
|
|
|
675
573
|
channel,
|
|
676
574
|
settings,
|
|
677
575
|
updateSettings,
|
|
678
|
-
markAllRead,
|
|
679
576
|
deleteChannel: deleteChannelAction,
|
|
680
577
|
feeds,
|
|
681
578
|
addFeed,
|
package/lib/storage/items.js
CHANGED
|
@@ -78,7 +78,6 @@ export async function addItem(application, { channelId, feedId, uid, item }) {
|
|
|
78
78
|
* @param {string} [options.after] - After cursor
|
|
79
79
|
* @param {number} [options.limit] - Items per page
|
|
80
80
|
* @param {string} [options.userId] - User ID for read state
|
|
81
|
-
* @param {boolean} [options.showRead] - Whether to show read items (default: false)
|
|
82
81
|
* @returns {Promise<object>} Timeline with items and paging
|
|
83
82
|
*/
|
|
84
83
|
export async function getTimelineItems(application, channelId, options = {}) {
|
|
@@ -87,12 +86,7 @@ export async function getTimelineItems(application, channelId, options = {}) {
|
|
|
87
86
|
typeof channelId === "string" ? new ObjectId(channelId) : channelId;
|
|
88
87
|
const limit = parseLimit(options.limit);
|
|
89
88
|
|
|
90
|
-
// Base query - filter out read items unless showRead is true
|
|
91
89
|
const baseQuery = { channelId: objectId };
|
|
92
|
-
if (options.userId && !options.showRead) {
|
|
93
|
-
baseQuery.readBy = { $ne: options.userId };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
90
|
const query = buildPaginationQuery({
|
|
97
91
|
before: options.before,
|
|
98
92
|
after: options.after,
|
|
@@ -262,24 +256,6 @@ export async function getItemsByUids(application, uids, userId) {
|
|
|
262
256
|
return items.map((item) => transformToJf2(item, userId));
|
|
263
257
|
}
|
|
264
258
|
|
|
265
|
-
/**
|
|
266
|
-
* Count read items in a channel for a user
|
|
267
|
-
* @param {object} application - Indiekit application
|
|
268
|
-
* @param {ObjectId|string} channelId - Channel ObjectId
|
|
269
|
-
* @param {string} userId - User ID
|
|
270
|
-
* @returns {Promise<number>} Count of read items
|
|
271
|
-
*/
|
|
272
|
-
export async function countReadItems(application, channelId, userId) {
|
|
273
|
-
const collection = getCollection(application);
|
|
274
|
-
const objectId =
|
|
275
|
-
typeof channelId === "string" ? new ObjectId(channelId) : channelId;
|
|
276
|
-
|
|
277
|
-
return collection.countDocuments({
|
|
278
|
-
channelId: objectId,
|
|
279
|
-
readBy: userId,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
259
|
/**
|
|
284
260
|
* Mark items as read
|
|
285
261
|
* @param {object} application - Indiekit application
|
package/locales/en.json
CHANGED
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
"title": "Reader",
|
|
5
5
|
"empty": "No items to display",
|
|
6
6
|
"markAllRead": "Mark all as read",
|
|
7
|
-
"showRead": "Show read ({{count}})",
|
|
8
|
-
"hideRead": "Hide read items",
|
|
9
|
-
"allRead": "All caught up!",
|
|
10
7
|
"newer": "Newer",
|
|
11
8
|
"older": "Older"
|
|
12
9
|
},
|
|
@@ -46,16 +43,12 @@
|
|
|
46
43
|
"compose": {
|
|
47
44
|
"title": "Compose",
|
|
48
45
|
"content": "What's on your mind?",
|
|
49
|
-
"comment": "Add a comment (optional)",
|
|
50
|
-
"commentHint": "Your comment will be included with the syndicated post",
|
|
51
46
|
"submit": "Post",
|
|
52
47
|
"cancel": "Cancel",
|
|
53
48
|
"replyTo": "Replying to",
|
|
54
49
|
"likeOf": "Liking",
|
|
55
50
|
"repostOf": "Reposting",
|
|
56
|
-
"bookmarkOf": "Bookmarking"
|
|
57
|
-
"syndicateTo": "Syndicate to",
|
|
58
|
-
"syndicateHint": "Also share this to your connected accounts"
|
|
51
|
+
"bookmarkOf": "Bookmarking"
|
|
59
52
|
},
|
|
60
53
|
"settings": {
|
|
61
54
|
"title": "{{channel}} settings",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmdes/indiekit-endpoint-microsub",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.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",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"reader",
|
|
11
11
|
"social-reader"
|
|
12
12
|
],
|
|
13
|
-
"homepage": "https://github.com/rmdes/indiekit",
|
|
13
|
+
"homepage": "https://github.com/rmdes/indiekit-endpoint-microsub",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Ricardo Mendes",
|
|
16
16
|
"url": "https://rmendes.net"
|
|
@@ -21,9 +21,6 @@
|
|
|
21
21
|
},
|
|
22
22
|
"type": "module",
|
|
23
23
|
"main": "index.js",
|
|
24
|
-
"scripts": {
|
|
25
|
-
"test": "node --test test/unit/*.js"
|
|
26
|
-
},
|
|
27
24
|
"files": [
|
|
28
25
|
"assets",
|
|
29
26
|
"lib",
|
|
@@ -32,12 +29,11 @@
|
|
|
32
29
|
"index.js"
|
|
33
30
|
],
|
|
34
31
|
"bugs": {
|
|
35
|
-
"url": "https://github.com/rmdes/indiekit/issues"
|
|
32
|
+
"url": "https://github.com/rmdes/indiekit-endpoint-microsub/issues"
|
|
36
33
|
},
|
|
37
34
|
"repository": {
|
|
38
35
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/rmdes/indiekit.git"
|
|
40
|
-
"directory": "packages/endpoint-microsub"
|
|
36
|
+
"url": "https://github.com/rmdes/indiekit-endpoint-microsub.git"
|
|
41
37
|
},
|
|
42
38
|
"dependencies": {
|
|
43
39
|
"@indiekit/error": "^1.0.0-beta.25",
|
package/views/channel.njk
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
{{ icon("previous") }} {{ __("microsub.channels.title") }}
|
|
8
8
|
</a>
|
|
9
9
|
<div class="channel__actions">
|
|
10
|
-
{% if not showRead and items.length > 0 %}
|
|
11
10
|
<form action="{{ baseUrl }}/api/mark-read" method="POST" style="display: inline;">
|
|
12
11
|
<input type="hidden" name="channel" value="{{ channel.uid }}">
|
|
13
12
|
<input type="hidden" name="entry" value="last-read-entry">
|
|
@@ -15,16 +14,6 @@
|
|
|
15
14
|
{{ icon("checkboxChecked") }} {{ __("microsub.reader.markAllRead") }}
|
|
16
15
|
</button>
|
|
17
16
|
</form>
|
|
18
|
-
{% endif %}
|
|
19
|
-
{% if showRead %}
|
|
20
|
-
<a href="{{ baseUrl }}/channels/{{ channel.uid }}" class="button button--secondary button--small">
|
|
21
|
-
{{ icon("hide") }} {{ __("microsub.reader.hideRead") }}
|
|
22
|
-
</a>
|
|
23
|
-
{% elif readCount > 0 %}
|
|
24
|
-
<a href="{{ baseUrl }}/channels/{{ channel.uid }}?showRead=true" class="button button--secondary button--small">
|
|
25
|
-
{{ icon("show") }} {{ __("microsub.reader.showRead", { count: readCount }) }}
|
|
26
|
-
</a>
|
|
27
|
-
{% endif %}
|
|
28
17
|
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--secondary button--small">
|
|
29
18
|
{{ icon("syndicate") }} {{ __("microsub.feeds.title") }}
|
|
30
19
|
</a>
|
|
@@ -59,19 +48,11 @@
|
|
|
59
48
|
{% endif %}
|
|
60
49
|
{% else %}
|
|
61
50
|
<div class="reader__empty">
|
|
62
|
-
{% if readCount > 0 and not showRead %}
|
|
63
|
-
{{ icon("checkboxChecked") }}
|
|
64
|
-
<p>{{ __("microsub.reader.allRead") }}</p>
|
|
65
|
-
<a href="{{ baseUrl }}/channels/{{ channel.uid }}?showRead=true" class="button button--secondary">
|
|
66
|
-
{{ icon("show") }} {{ __("microsub.reader.showRead", { count: readCount }) }}
|
|
67
|
-
</a>
|
|
68
|
-
{% else %}
|
|
69
51
|
{{ icon("syndicate") }}
|
|
70
52
|
<p>{{ __("microsub.timeline.empty") }}</p>
|
|
71
53
|
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--primary">
|
|
72
54
|
{{ __("microsub.feeds.subscribe") }}
|
|
73
55
|
</a>
|
|
74
|
-
{% endif %}
|
|
75
56
|
</div>
|
|
76
57
|
{% endif %}
|
|
77
58
|
</div>
|
package/views/compose.njk
CHANGED
|
@@ -71,32 +71,6 @@
|
|
|
71
71
|
<div class="compose__counter">
|
|
72
72
|
<span id="char-count">0</span> characters
|
|
73
73
|
</div>
|
|
74
|
-
{% else %}
|
|
75
|
-
{# Comment field for likes/reposts/bookmarks #}
|
|
76
|
-
{{ textarea({
|
|
77
|
-
label: __("microsub.compose.comment"),
|
|
78
|
-
id: "content",
|
|
79
|
-
name: "content",
|
|
80
|
-
rows: 3,
|
|
81
|
-
hint: __("microsub.compose.commentHint")
|
|
82
|
-
}) }}
|
|
83
|
-
<div class="compose__counter">
|
|
84
|
-
<span id="char-count">0</span> characters
|
|
85
|
-
</div>
|
|
86
|
-
{% endif %}
|
|
87
|
-
|
|
88
|
-
{# Syndication targets #}
|
|
89
|
-
{% if syndicationTargets and syndicationTargets.length %}
|
|
90
|
-
<fieldset class="compose__syndication">
|
|
91
|
-
<legend>{{ __("microsub.compose.syndicateTo") }}</legend>
|
|
92
|
-
<p class="hint">{{ __("microsub.compose.syndicateHint") }}</p>
|
|
93
|
-
{% for target in syndicationTargets %}
|
|
94
|
-
<label class="syndication-target">
|
|
95
|
-
<input type="checkbox" name="mp-syndicate-to" value="{{ target.uid }}"{% if target.checked %} checked{% endif %}>
|
|
96
|
-
<span class="syndication-target__name">{{ target.name }}</span>
|
|
97
|
-
</label>
|
|
98
|
-
{% endfor %}
|
|
99
|
-
</fieldset>
|
|
100
74
|
{% endif %}
|
|
101
75
|
|
|
102
76
|
<div class="button-group">
|
|
@@ -110,6 +84,7 @@
|
|
|
110
84
|
</form>
|
|
111
85
|
</div>
|
|
112
86
|
|
|
87
|
+
{% if not isAction %}
|
|
113
88
|
<script type="module">
|
|
114
89
|
const textarea = document.getElementById('content');
|
|
115
90
|
const counter = document.getElementById('char-count');
|
|
@@ -119,4 +94,5 @@
|
|
|
119
94
|
});
|
|
120
95
|
}
|
|
121
96
|
</script>
|
|
97
|
+
{% endif %}
|
|
122
98
|
{% endblock %}
|
package/README.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# @indiekit/endpoint-microsub
|
|
2
|
-
|
|
3
|
-
Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the [Microsub protocol](https://indieweb.org/Microsub).
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Full Microsub API** - Channels, timeline, follow/unfollow, search, preview, mute, block
|
|
8
|
-
- **Multiple feed formats** - RSS 1.0/2.0, Atom, JSON Feed, h-feed (Microformats)
|
|
9
|
-
- **External client support** - Works with Monocle, Together, IndiePass
|
|
10
|
-
- **Built-in reader UI** - Social reading experience in Indiekit admin
|
|
11
|
-
- **Real-time updates** - Server-Sent Events (SSE) and WebSub integration
|
|
12
|
-
- **Adaptive polling** - Tier-based feed fetching inspired by Ekster
|
|
13
|
-
- **Direct webmention receiving** - Notifications channel for mentions
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
16
|
-
|
|
17
|
-
`npm install @indiekit/endpoint-microsub`
|
|
18
|
-
|
|
19
|
-
## Usage
|
|
20
|
-
|
|
21
|
-
Add `@indiekit/endpoint-microsub` to the list of plugins in your configuration:
|
|
22
|
-
|
|
23
|
-
```js
|
|
24
|
-
export default {
|
|
25
|
-
plugins: ["@indiekit/endpoint-microsub"],
|
|
26
|
-
};
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Options
|
|
30
|
-
|
|
31
|
-
| Option | Type | Description |
|
|
32
|
-
| :----------- | :------- | :--------------------------------------------------------------- |
|
|
33
|
-
| `mountPath` | `string` | Path to mount Microsub API. _Optional_, defaults to `/microsub`. |
|
|
34
|
-
| `readerPath` | `string` | Path to mount reader UI. _Optional_, defaults to `/reader`. |
|
|
35
|
-
|
|
36
|
-
## Endpoints
|
|
37
|
-
|
|
38
|
-
### Microsub API
|
|
39
|
-
|
|
40
|
-
The main Microsub endpoint is mounted at `/microsub` (configurable).
|
|
41
|
-
|
|
42
|
-
**Discovery**: Add this to your site's `<head>`:
|
|
43
|
-
|
|
44
|
-
```html
|
|
45
|
-
<link rel="microsub" href="https://yoursite.com/microsub" />
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Supported actions
|
|
49
|
-
|
|
50
|
-
| Action | GET | POST | Description |
|
|
51
|
-
| :--------- | :-- | :--- | :--------------------------------------------- |
|
|
52
|
-
| `channels` | ✓ | ✓ | List, create, update, delete, reorder channels |
|
|
53
|
-
| `timeline` | ✓ | ✓ | Get timeline, mark read/unread, remove items |
|
|
54
|
-
| `follow` | ✓ | ✓ | List followed feeds, subscribe to new feeds |
|
|
55
|
-
| `unfollow` | - | ✓ | Unsubscribe from feeds |
|
|
56
|
-
| `search` | ✓ | ✓ | Feed discovery and full-text search |
|
|
57
|
-
| `preview` | ✓ | ✓ | Preview feed before subscribing |
|
|
58
|
-
| `mute` | ✓ | ✓ | List muted URLs, mute/unmute |
|
|
59
|
-
| `block` | ✓ | ✓ | List blocked URLs, block/unblock |
|
|
60
|
-
| `events` | ✓ | - | Server-Sent Events stream |
|
|
61
|
-
|
|
62
|
-
### Reader UI
|
|
63
|
-
|
|
64
|
-
The built-in reader is mounted at `/reader` (configurable) and provides:
|
|
65
|
-
|
|
66
|
-
- Channel list with unread counts
|
|
67
|
-
- Timeline view with items
|
|
68
|
-
- Mark as read on scroll/click
|
|
69
|
-
- Like/reply/repost via Micropub
|
|
70
|
-
- Channel settings (filters)
|
|
71
|
-
- Compose modal
|
|
72
|
-
|
|
73
|
-
### WebSub callbacks
|
|
74
|
-
|
|
75
|
-
WebSub hub callbacks are handled at `/microsub/websub/:id`.
|
|
76
|
-
|
|
77
|
-
### Webmention receiving
|
|
78
|
-
|
|
79
|
-
Direct webmentions can be sent to `/microsub/webmention`.
|
|
80
|
-
|
|
81
|
-
## MongoDB Collections
|
|
82
|
-
|
|
83
|
-
This plugin creates the following collections:
|
|
84
|
-
|
|
85
|
-
- `microsub_channels` - User's feed channels
|
|
86
|
-
- `microsub_feeds` - Subscribed feeds
|
|
87
|
-
- `microsub_items` - Timeline entries
|
|
88
|
-
- `microsub_notifications` - Webmention notifications
|
|
89
|
-
- `microsub_muted` - Muted URLs
|
|
90
|
-
- `microsub_blocked` - Blocked URLs
|
|
91
|
-
|
|
92
|
-
## Dependencies
|
|
93
|
-
|
|
94
|
-
- **feedparser** - RSS/Atom parsing
|
|
95
|
-
- **microformats-parser** - h-feed parsing
|
|
96
|
-
- **ioredis** - Redis client (optional, for caching/pub-sub)
|
|
97
|
-
- **sanitize-html** - XSS prevention
|
|
98
|
-
|
|
99
|
-
## External Clients
|
|
100
|
-
|
|
101
|
-
This endpoint is compatible with:
|
|
102
|
-
|
|
103
|
-
- [Monocle](https://monocle.p3k.io/) - Web-based reader
|
|
104
|
-
- [Together](https://together.tpxl.io/) - Web-based reader
|
|
105
|
-
- [IndiePass](https://indiepass.app/) - Mobile/desktop app (archived)
|
|
106
|
-
|
|
107
|
-
## References
|
|
108
|
-
|
|
109
|
-
- [Microsub Specification](https://indieweb.org/Microsub-spec)
|
|
110
|
-
- [Ekster](https://github.com/pstuifzand/ekster) - Reference implementation in Go
|
|
111
|
-
- [Aperture](https://github.com/aaronpk/Aperture) - Popular Microsub server in PHP
|