@rmdes/indiekit-endpoint-microsub 1.0.0-beta.7 → 1.0.0-beta.9
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 +12 -0
- package/lib/controllers/reader.js +87 -0
- package/package.json +1 -1
- package/views/reader.njk +3 -0
- package/views/search.njk +58 -0
package/index.js
CHANGED
|
@@ -87,6 +87,9 @@ export default class MicrosubEndpoint {
|
|
|
87
87
|
readerRouter.get("/item/:id", readerController.item);
|
|
88
88
|
readerRouter.get("/compose", readerController.compose);
|
|
89
89
|
readerRouter.post("/compose", readerController.submitCompose);
|
|
90
|
+
readerRouter.get("/search", readerController.searchPage);
|
|
91
|
+
readerRouter.post("/search", readerController.searchFeeds);
|
|
92
|
+
readerRouter.post("/subscribe", readerController.subscribe);
|
|
90
93
|
router.use("/reader", readerRouter);
|
|
91
94
|
|
|
92
95
|
return router;
|
|
@@ -114,6 +117,8 @@ export default class MicrosubEndpoint {
|
|
|
114
117
|
* @param {object} indiekit - Indiekit instance
|
|
115
118
|
*/
|
|
116
119
|
init(indiekit) {
|
|
120
|
+
console.info("[Microsub] Initializing endpoint-microsub plugin");
|
|
121
|
+
|
|
117
122
|
// Register MongoDB collections
|
|
118
123
|
indiekit.addCollection("microsub_channels");
|
|
119
124
|
indiekit.addCollection("microsub_feeds");
|
|
@@ -122,6 +127,8 @@ export default class MicrosubEndpoint {
|
|
|
122
127
|
indiekit.addCollection("microsub_muted");
|
|
123
128
|
indiekit.addCollection("microsub_blocked");
|
|
124
129
|
|
|
130
|
+
console.info("[Microsub] Registered MongoDB collections");
|
|
131
|
+
|
|
125
132
|
// Register endpoint
|
|
126
133
|
indiekit.addEndpoint(this);
|
|
127
134
|
|
|
@@ -133,7 +140,12 @@ export default class MicrosubEndpoint {
|
|
|
133
140
|
// Start feed polling scheduler when server starts
|
|
134
141
|
// This will be called after the server is ready
|
|
135
142
|
if (indiekit.database) {
|
|
143
|
+
console.info("[Microsub] Database available, starting scheduler");
|
|
136
144
|
startScheduler(indiekit);
|
|
145
|
+
} else {
|
|
146
|
+
console.warn(
|
|
147
|
+
"[Microsub] Database not available at init, scheduler not started",
|
|
148
|
+
);
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @module controllers/reader
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { discoverFeedsFromUrl } from "../feeds/fetcher.js";
|
|
6
7
|
import { refreshFeedNow } from "../polling/scheduler.js";
|
|
7
8
|
import {
|
|
8
9
|
getChannels,
|
|
@@ -302,6 +303,89 @@ export async function submitCompose(request, response) {
|
|
|
302
303
|
response.redirect(`${request.baseUrl}/channels`);
|
|
303
304
|
}
|
|
304
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Search/discover feeds page
|
|
308
|
+
* @param {object} request - Express request
|
|
309
|
+
* @param {object} response - Express response
|
|
310
|
+
* @returns {Promise<void>}
|
|
311
|
+
*/
|
|
312
|
+
export async function searchPage(request, response) {
|
|
313
|
+
const { application } = request.app.locals;
|
|
314
|
+
const userId = request.session?.userId;
|
|
315
|
+
|
|
316
|
+
const channelList = await getChannels(application, userId);
|
|
317
|
+
|
|
318
|
+
response.render("search", {
|
|
319
|
+
title: request.__("microsub.search.title"),
|
|
320
|
+
channels: channelList,
|
|
321
|
+
baseUrl: request.baseUrl,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Search for feeds from URL
|
|
327
|
+
* @param {object} request - Express request
|
|
328
|
+
* @param {object} response - Express response
|
|
329
|
+
* @returns {Promise<void>}
|
|
330
|
+
*/
|
|
331
|
+
export async function searchFeeds(request, response) {
|
|
332
|
+
const { application } = request.app.locals;
|
|
333
|
+
const userId = request.session?.userId;
|
|
334
|
+
const { query } = request.body;
|
|
335
|
+
|
|
336
|
+
const channelList = await getChannels(application, userId);
|
|
337
|
+
|
|
338
|
+
let results = [];
|
|
339
|
+
if (query) {
|
|
340
|
+
try {
|
|
341
|
+
results = await discoverFeedsFromUrl(query);
|
|
342
|
+
} catch {
|
|
343
|
+
// Ignore discovery errors
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
response.render("search", {
|
|
348
|
+
title: request.__("microsub.search.title"),
|
|
349
|
+
channels: channelList,
|
|
350
|
+
query,
|
|
351
|
+
results,
|
|
352
|
+
searched: true,
|
|
353
|
+
baseUrl: request.baseUrl,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Subscribe to a feed from search results
|
|
359
|
+
* @param {object} request - Express request
|
|
360
|
+
* @param {object} response - Express response
|
|
361
|
+
* @returns {Promise<void>}
|
|
362
|
+
*/
|
|
363
|
+
export async function subscribe(request, response) {
|
|
364
|
+
const { application } = request.app.locals;
|
|
365
|
+
const userId = request.session?.userId;
|
|
366
|
+
const { url, channel: channelUid } = request.body;
|
|
367
|
+
|
|
368
|
+
const channelDocument = await getChannel(application, channelUid, userId);
|
|
369
|
+
if (!channelDocument) {
|
|
370
|
+
return response.status(404).render("404");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Create feed subscription
|
|
374
|
+
const feed = await createFeed(application, {
|
|
375
|
+
channelId: channelDocument._id,
|
|
376
|
+
url,
|
|
377
|
+
title: undefined,
|
|
378
|
+
photo: undefined,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// Trigger immediate fetch in background
|
|
382
|
+
refreshFeedNow(application, feed._id).catch((error) => {
|
|
383
|
+
console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
response.redirect(`${request.baseUrl}/channels/${channelUid}/feeds`);
|
|
387
|
+
}
|
|
388
|
+
|
|
305
389
|
export const readerController = {
|
|
306
390
|
index,
|
|
307
391
|
channels,
|
|
@@ -316,4 +400,7 @@ export const readerController = {
|
|
|
316
400
|
item,
|
|
317
401
|
compose,
|
|
318
402
|
submitCompose,
|
|
403
|
+
searchPage,
|
|
404
|
+
searchFeeds,
|
|
405
|
+
subscribe,
|
|
319
406
|
};
|
package/package.json
CHANGED
package/views/reader.njk
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
{% endfor %}
|
|
22
22
|
</ul>
|
|
23
23
|
<p class="reader__actions">
|
|
24
|
+
<a href="{{ baseUrl }}/search" class="button button--primary">
|
|
25
|
+
{{ icon("syndicate") }} {{ __("microsub.feeds.follow") }}
|
|
26
|
+
</a>
|
|
24
27
|
<a href="{{ baseUrl }}/channels/new" class="button button--secondary">
|
|
25
28
|
{{ icon("createPost") }} {{ __("microsub.channels.new") }}
|
|
26
29
|
</a>
|
package/views/search.njk
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{% extends "document.njk" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="search">
|
|
5
|
+
<a href="{{ baseUrl }}/channels" class="back-link">
|
|
6
|
+
{{ icon("previous") }} {{ __("microsub.channels.title") }}
|
|
7
|
+
</a>
|
|
8
|
+
|
|
9
|
+
<h2>{{ __("microsub.search.title") }}</h2>
|
|
10
|
+
|
|
11
|
+
<form method="post" action="{{ baseUrl }}/search" class="search__form">
|
|
12
|
+
{{ input({
|
|
13
|
+
id: "query",
|
|
14
|
+
name: "query",
|
|
15
|
+
label: __("microsub.search.placeholder"),
|
|
16
|
+
type: "url",
|
|
17
|
+
required: true,
|
|
18
|
+
placeholder: "https://example.com",
|
|
19
|
+
autocomplete: "off",
|
|
20
|
+
value: query,
|
|
21
|
+
attributes: { autofocus: true }
|
|
22
|
+
}) }}
|
|
23
|
+
<div class="button-group">
|
|
24
|
+
{{ button({ text: __("microsub.search.submit") }) }}
|
|
25
|
+
</div>
|
|
26
|
+
</form>
|
|
27
|
+
|
|
28
|
+
{% if results and results.length > 0 %}
|
|
29
|
+
<div class="search__results">
|
|
30
|
+
<h3>{{ __("microsub.search.title") }}</h3>
|
|
31
|
+
<ul class="search__list">
|
|
32
|
+
{% for result in results %}
|
|
33
|
+
<li class="search__item">
|
|
34
|
+
<div class="search__feed">
|
|
35
|
+
<span class="search__url">{{ result.url }}</span>
|
|
36
|
+
</div>
|
|
37
|
+
<form method="post" action="{{ baseUrl }}/subscribe" class="search__subscribe">
|
|
38
|
+
<input type="hidden" name="url" value="{{ result.url }}">
|
|
39
|
+
<label for="channel-{{ loop.index }}" class="visually-hidden">{{ __("microsub.channels.title") }}</label>
|
|
40
|
+
<select name="channel" id="channel-{{ loop.index }}" class="select select--small">
|
|
41
|
+
{% for channel in channels %}
|
|
42
|
+
<option value="{{ channel.uid }}">{{ channel.name }}</option>
|
|
43
|
+
{% endfor %}
|
|
44
|
+
</select>
|
|
45
|
+
{{ button({
|
|
46
|
+
text: __("microsub.feeds.follow"),
|
|
47
|
+
classes: "button--small"
|
|
48
|
+
}) }}
|
|
49
|
+
</form>
|
|
50
|
+
</li>
|
|
51
|
+
{% endfor %}
|
|
52
|
+
</ul>
|
|
53
|
+
</div>
|
|
54
|
+
{% elif searched %}
|
|
55
|
+
{{ prose({ text: __("microsub.search.noResults") }) }}
|
|
56
|
+
{% endif %}
|
|
57
|
+
</div>
|
|
58
|
+
{% endblock %}
|