@rmdes/indiekit-endpoint-microsub 1.0.55 → 1.0.57
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/reader.js +408 -0
- package/index.js +61 -49
- package/lib/activitypub/outbox-fetcher.js +14 -2
- package/lib/cache/redis.js +26 -7
- package/lib/controllers/channels.js +2 -2
- package/lib/controllers/reader/actor.js +142 -0
- package/lib/controllers/reader/channel.js +301 -0
- package/lib/controllers/reader/compose.js +242 -0
- package/lib/controllers/reader/deck.js +129 -0
- package/lib/controllers/reader/feed-repair.js +117 -0
- package/lib/controllers/reader/feed.js +246 -0
- package/lib/controllers/reader/index.js +126 -0
- package/lib/controllers/reader/search.js +157 -0
- package/lib/controllers/reader/timeline.js +250 -0
- package/lib/controllers/search.js +6 -0
- package/lib/controllers/timeline.js +6 -4
- package/lib/feeds/atom.js +1 -1
- package/lib/feeds/capabilities.js +5 -0
- package/lib/feeds/fetcher.js +5 -28
- package/lib/feeds/hfeed.js +1 -1
- package/lib/feeds/jsonfeed.js +1 -1
- package/lib/feeds/normalizer-hfeed.js +209 -0
- package/lib/feeds/normalizer-jsonfeed.js +171 -0
- package/lib/feeds/normalizer-rss.js +178 -0
- package/lib/feeds/normalizer.js +22 -614
- package/lib/feeds/rss.js +1 -1
- package/lib/media/proxy.js +82 -27
- package/lib/polling/processor.js +30 -21
- package/lib/polling/scheduler.js +2 -0
- package/lib/realtime/broker.js +6 -1
- package/lib/storage/channels.js +53 -42
- package/lib/storage/feeds.js +3 -1
- package/lib/storage/items-read-state.js +287 -0
- package/lib/storage/items-retention.js +174 -0
- package/lib/storage/items-search.js +34 -0
- package/lib/storage/items.js +113 -610
- package/lib/storage/read-state.js +1 -1
- package/lib/utils/async-handler.js +7 -0
- package/lib/utils/constants.js +7 -0
- package/lib/utils/csrf.js +51 -0
- package/lib/utils/html.js +25 -0
- package/lib/utils/sanitize.js +61 -0
- package/lib/utils/source-type.js +28 -0
- package/lib/utils/validation.js +8 -2
- package/lib/webmention/processor.js +1 -1
- package/lib/webmention/verifier.js +10 -21
- package/lib/websub/subscriber.js +12 -0
- package/locales/de.json +3 -0
- package/locales/en.json +2 -0
- package/locales/es-419.json +3 -0
- package/locales/es.json +3 -0
- package/locales/fr.json +3 -0
- package/locales/hi.json +3 -0
- package/locales/id.json +3 -0
- package/locales/it.json +3 -0
- package/locales/nl.json +3 -0
- package/locales/pl.json +3 -0
- package/locales/pt-BR.json +3 -0
- package/locales/pt.json +3 -0
- package/locales/sr.json +3 -0
- package/locales/sv.json +3 -0
- package/locales/zh-Hans-CN.json +3 -0
- package/package.json +3 -1
- package/views/actor.njk +2 -0
- package/views/channel-new.njk +1 -0
- package/views/channel.njk +3 -344
- package/views/compose.njk +1 -0
- package/views/deck-settings.njk +1 -0
- package/views/feed-edit.njk +3 -0
- package/views/feeds.njk +4 -0
- package/views/layouts/reader.njk +1 -0
- package/views/search.njk +2 -0
- package/views/settings.njk +2 -0
- package/views/timeline.njk +3 -271
- package/lib/controllers/reader.js +0 -1580
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feed management
|
|
3
|
+
* @module controllers/reader/feed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { validateFeedUrl } from "../../feeds/validator.js";
|
|
7
|
+
import { refreshFeedNow } from "../../polling/scheduler.js";
|
|
8
|
+
import { getChannel } from "../../storage/channels.js";
|
|
9
|
+
import {
|
|
10
|
+
getFeedsForChannel,
|
|
11
|
+
getFeedById,
|
|
12
|
+
createFeed,
|
|
13
|
+
deleteFeed,
|
|
14
|
+
updateFeed,
|
|
15
|
+
} from "../../storage/feeds.js";
|
|
16
|
+
import { getUserId } from "../../utils/auth.js";
|
|
17
|
+
|
|
18
|
+
export { rediscoverFeed, refreshFeed } from "./feed-repair.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* View feeds for a channel
|
|
22
|
+
* @param {object} request - Express request
|
|
23
|
+
* @param {object} response - Express response
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
*/
|
|
26
|
+
export async function feeds(request, response) {
|
|
27
|
+
const { application } = request.app.locals;
|
|
28
|
+
const userId = getUserId(request);
|
|
29
|
+
const { uid } = request.params;
|
|
30
|
+
|
|
31
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
32
|
+
if (!channelDocument) {
|
|
33
|
+
return response.status(404).render("404");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const feedList = await getFeedsForChannel(application, channelDocument._id);
|
|
37
|
+
|
|
38
|
+
response.render("feeds", {
|
|
39
|
+
title: request.__("microsub.feeds.title"),
|
|
40
|
+
channel: channelDocument,
|
|
41
|
+
feeds: feedList,
|
|
42
|
+
baseUrl: request.baseUrl,
|
|
43
|
+
readerBaseUrl: request.baseUrl,
|
|
44
|
+
activeView: "channels",
|
|
45
|
+
breadcrumbs: [
|
|
46
|
+
{ text: "Reader", href: request.baseUrl },
|
|
47
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
48
|
+
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
|
49
|
+
{ text: "Feeds" },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Add feed to channel
|
|
56
|
+
* @param {object} request - Express request
|
|
57
|
+
* @param {object} response - Express response
|
|
58
|
+
* @returns {Promise<void>}
|
|
59
|
+
*/
|
|
60
|
+
export async function addFeed(request, response) {
|
|
61
|
+
const { application } = request.app.locals;
|
|
62
|
+
const userId = getUserId(request);
|
|
63
|
+
const { uid } = request.params;
|
|
64
|
+
const { url } = request.body;
|
|
65
|
+
|
|
66
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
67
|
+
if (!channelDocument) {
|
|
68
|
+
return response.status(404).render("404");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Create feed subscription (throws DUPLICATE_FEED if already exists)
|
|
73
|
+
const feed = await createFeed(application, {
|
|
74
|
+
channelId: channelDocument._id,
|
|
75
|
+
url,
|
|
76
|
+
title: undefined,
|
|
77
|
+
photo: undefined,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Trigger immediate fetch in background
|
|
81
|
+
refreshFeedNow(application, feed._id).catch((error) => {
|
|
82
|
+
console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error.code === "DUPLICATE_FEED") {
|
|
88
|
+
// Re-render feeds page with error message
|
|
89
|
+
const feedList = await getFeedsForChannel(application, channelDocument._id);
|
|
90
|
+
return response.render("feeds", {
|
|
91
|
+
title: request.__("microsub.feeds.title"),
|
|
92
|
+
channel: channelDocument,
|
|
93
|
+
feeds: feedList,
|
|
94
|
+
baseUrl: request.baseUrl,
|
|
95
|
+
readerBaseUrl: request.baseUrl,
|
|
96
|
+
activeView: "channels",
|
|
97
|
+
error: `This feed already exists in channel "${error.channelName}"`,
|
|
98
|
+
breadcrumbs: [
|
|
99
|
+
{ text: "Reader", href: request.baseUrl },
|
|
100
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
101
|
+
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
|
102
|
+
{ text: "Feeds" },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remove feed from channel
|
|
112
|
+
* @param {object} request - Express request
|
|
113
|
+
* @param {object} response - Express response
|
|
114
|
+
* @returns {Promise<void>}
|
|
115
|
+
*/
|
|
116
|
+
export async function removeFeed(request, response) {
|
|
117
|
+
const { application } = request.app.locals;
|
|
118
|
+
const userId = getUserId(request);
|
|
119
|
+
const { uid } = request.params;
|
|
120
|
+
const { url } = request.body;
|
|
121
|
+
|
|
122
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
123
|
+
if (!channelDocument) {
|
|
124
|
+
return response.status(404).render("404");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await deleteFeed(application, channelDocument._id, url);
|
|
128
|
+
|
|
129
|
+
response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* View single feed details with status - redirects to edit form
|
|
134
|
+
* @param {object} request - Express request
|
|
135
|
+
* @param {object} response - Express response
|
|
136
|
+
* @returns {Promise<void>}
|
|
137
|
+
*/
|
|
138
|
+
export async function feedDetails(request, response) {
|
|
139
|
+
const { uid, feedId } = request.params;
|
|
140
|
+
// Redirect to edit form which shows all details
|
|
141
|
+
response.redirect(`${request.baseUrl}/channels/${uid}/feeds/${feedId}/edit`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Edit feed URL form
|
|
146
|
+
* @param {object} request - Express request
|
|
147
|
+
* @param {object} response - Express response
|
|
148
|
+
* @returns {Promise<void>}
|
|
149
|
+
*/
|
|
150
|
+
export async function editFeedForm(request, response) {
|
|
151
|
+
const { application } = request.app.locals;
|
|
152
|
+
const userId = getUserId(request);
|
|
153
|
+
const { uid, feedId } = request.params;
|
|
154
|
+
|
|
155
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
156
|
+
if (!channelDocument) {
|
|
157
|
+
return response.status(404).render("404");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const feed = await getFeedById(application, feedId);
|
|
161
|
+
if (!feed || feed.channelId.toString() !== channelDocument._id.toString()) {
|
|
162
|
+
return response.status(404).render("404");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
response.render("feed-edit", {
|
|
166
|
+
title: request.__("microsub.feeds.edit"),
|
|
167
|
+
channel: channelDocument,
|
|
168
|
+
feed,
|
|
169
|
+
baseUrl: request.baseUrl,
|
|
170
|
+
readerBaseUrl: request.baseUrl,
|
|
171
|
+
activeView: "channels",
|
|
172
|
+
breadcrumbs: [
|
|
173
|
+
{ text: "Reader", href: request.baseUrl },
|
|
174
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
175
|
+
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
|
176
|
+
{ text: "Feeds", href: `${request.baseUrl}/channels/${uid}/feeds` },
|
|
177
|
+
{ text: "Edit" },
|
|
178
|
+
],
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Update feed URL
|
|
184
|
+
* @param {object} request - Express request
|
|
185
|
+
* @param {object} response - Express response
|
|
186
|
+
* @returns {Promise<void>}
|
|
187
|
+
*/
|
|
188
|
+
export async function updateFeedUrl(request, response) {
|
|
189
|
+
const { application } = request.app.locals;
|
|
190
|
+
const userId = getUserId(request);
|
|
191
|
+
const { uid, feedId } = request.params;
|
|
192
|
+
const { url: newUrl } = request.body;
|
|
193
|
+
|
|
194
|
+
const channelDocument = await getChannel(application, uid, userId);
|
|
195
|
+
if (!channelDocument) {
|
|
196
|
+
return response.status(404).render("404");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const feed = await getFeedById(application, feedId);
|
|
200
|
+
if (!feed || feed.channelId.toString() !== channelDocument._id.toString()) {
|
|
201
|
+
return response.status(404).render("404");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Validate the new URL is a valid feed
|
|
205
|
+
const validation = await validateFeedUrl(newUrl);
|
|
206
|
+
|
|
207
|
+
if (!validation.valid) {
|
|
208
|
+
return response.render("feed-edit", {
|
|
209
|
+
title: request.__("microsub.feeds.edit"),
|
|
210
|
+
channel: channelDocument,
|
|
211
|
+
feed,
|
|
212
|
+
error: validation.error,
|
|
213
|
+
baseUrl: request.baseUrl,
|
|
214
|
+
readerBaseUrl: request.baseUrl,
|
|
215
|
+
activeView: "channels",
|
|
216
|
+
breadcrumbs: [
|
|
217
|
+
{ text: "Reader", href: request.baseUrl },
|
|
218
|
+
{ text: "Channels", href: `${request.baseUrl}/channels` },
|
|
219
|
+
{ text: channelDocument.name, href: `${request.baseUrl}/channels/${uid}` },
|
|
220
|
+
{ text: "Feeds", href: `${request.baseUrl}/channels/${uid}/feeds` },
|
|
221
|
+
{ text: "Edit" },
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Update the feed URL and reset error state
|
|
227
|
+
await updateFeed(application, feedId, {
|
|
228
|
+
url: newUrl,
|
|
229
|
+
title: validation.title || feed.title,
|
|
230
|
+
status: "active",
|
|
231
|
+
lastError: undefined,
|
|
232
|
+
lastErrorAt: undefined,
|
|
233
|
+
consecutiveErrors: 0,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Trigger immediate fetch
|
|
237
|
+
refreshFeedNow(application, feedId).catch((error) => {
|
|
238
|
+
console.error(
|
|
239
|
+
`[Microsub] Error refreshing updated feed ${newUrl}:`,
|
|
240
|
+
error.message,
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
|
|
245
|
+
}
|
|
246
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader controller - barrel re-export
|
|
3
|
+
* @module controllers/reader
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
index,
|
|
8
|
+
channels,
|
|
9
|
+
newChannel,
|
|
10
|
+
createChannelAction,
|
|
11
|
+
channel,
|
|
12
|
+
channelHtml,
|
|
13
|
+
settings,
|
|
14
|
+
updateSettings,
|
|
15
|
+
deleteChannelAction,
|
|
16
|
+
} from "./channel.js";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
feeds,
|
|
20
|
+
addFeed,
|
|
21
|
+
removeFeed,
|
|
22
|
+
feedDetails,
|
|
23
|
+
editFeedForm,
|
|
24
|
+
updateFeedUrl,
|
|
25
|
+
rediscoverFeed,
|
|
26
|
+
refreshFeed,
|
|
27
|
+
} from "./feed.js";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
timeline,
|
|
31
|
+
timelineHtml,
|
|
32
|
+
markAllRead,
|
|
33
|
+
markViewRead,
|
|
34
|
+
item,
|
|
35
|
+
} from "./timeline.js";
|
|
36
|
+
|
|
37
|
+
export { compose, submitCompose } from "./compose.js";
|
|
38
|
+
|
|
39
|
+
export { searchPage, searchFeeds, subscribe } from "./search.js";
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
actorProfile,
|
|
43
|
+
followActorAction,
|
|
44
|
+
unfollowActorAction,
|
|
45
|
+
} from "./actor.js";
|
|
46
|
+
|
|
47
|
+
export { deck, deckSettings, saveDeckSettings } from "./deck.js";
|
|
48
|
+
|
|
49
|
+
import {
|
|
50
|
+
index,
|
|
51
|
+
channels,
|
|
52
|
+
newChannel,
|
|
53
|
+
createChannelAction,
|
|
54
|
+
channel,
|
|
55
|
+
channelHtml,
|
|
56
|
+
settings,
|
|
57
|
+
updateSettings,
|
|
58
|
+
deleteChannelAction,
|
|
59
|
+
} from "./channel.js";
|
|
60
|
+
|
|
61
|
+
import {
|
|
62
|
+
feeds,
|
|
63
|
+
addFeed,
|
|
64
|
+
removeFeed,
|
|
65
|
+
feedDetails,
|
|
66
|
+
editFeedForm,
|
|
67
|
+
updateFeedUrl,
|
|
68
|
+
rediscoverFeed,
|
|
69
|
+
refreshFeed,
|
|
70
|
+
} from "./feed.js";
|
|
71
|
+
|
|
72
|
+
import {
|
|
73
|
+
timeline,
|
|
74
|
+
timelineHtml,
|
|
75
|
+
markAllRead,
|
|
76
|
+
markViewRead,
|
|
77
|
+
item,
|
|
78
|
+
} from "./timeline.js";
|
|
79
|
+
|
|
80
|
+
import { compose, submitCompose } from "./compose.js";
|
|
81
|
+
|
|
82
|
+
import { searchPage, searchFeeds, subscribe } from "./search.js";
|
|
83
|
+
|
|
84
|
+
import {
|
|
85
|
+
actorProfile,
|
|
86
|
+
followActorAction,
|
|
87
|
+
unfollowActorAction,
|
|
88
|
+
} from "./actor.js";
|
|
89
|
+
|
|
90
|
+
import { deck, deckSettings, saveDeckSettings } from "./deck.js";
|
|
91
|
+
|
|
92
|
+
export const readerController = {
|
|
93
|
+
index,
|
|
94
|
+
channels,
|
|
95
|
+
newChannel,
|
|
96
|
+
createChannel: createChannelAction,
|
|
97
|
+
channel,
|
|
98
|
+
channelHtml,
|
|
99
|
+
settings,
|
|
100
|
+
updateSettings,
|
|
101
|
+
markAllRead,
|
|
102
|
+
markViewRead,
|
|
103
|
+
deleteChannel: deleteChannelAction,
|
|
104
|
+
feeds,
|
|
105
|
+
addFeed,
|
|
106
|
+
removeFeed,
|
|
107
|
+
feedDetails,
|
|
108
|
+
editFeedForm,
|
|
109
|
+
updateFeedUrl,
|
|
110
|
+
rediscoverFeed,
|
|
111
|
+
refreshFeed,
|
|
112
|
+
item,
|
|
113
|
+
compose,
|
|
114
|
+
submitCompose,
|
|
115
|
+
searchPage,
|
|
116
|
+
searchFeeds,
|
|
117
|
+
subscribe,
|
|
118
|
+
actorProfile,
|
|
119
|
+
followActorAction,
|
|
120
|
+
unfollowActorAction,
|
|
121
|
+
timeline,
|
|
122
|
+
timelineHtml,
|
|
123
|
+
deck,
|
|
124
|
+
deckSettings,
|
|
125
|
+
saveDeckSettings,
|
|
126
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feed discovery UI
|
|
3
|
+
* @module controllers/reader/search
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { discoverAndValidateFeeds } from "../../feeds/discovery.js";
|
|
7
|
+
import { validateFeedUrl } from "../../feeds/validator.js";
|
|
8
|
+
import { refreshFeedNow } from "../../polling/scheduler.js";
|
|
9
|
+
import { getChannels, getChannel } from "../../storage/channels.js";
|
|
10
|
+
import { createFeed } from "../../storage/feeds.js";
|
|
11
|
+
import { getUserId } from "../../utils/auth.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Search/discover feeds page
|
|
15
|
+
* @param {object} request - Express request
|
|
16
|
+
* @param {object} response - Express response
|
|
17
|
+
* @returns {Promise<void>}
|
|
18
|
+
*/
|
|
19
|
+
export async function searchPage(request, response) {
|
|
20
|
+
const { application } = request.app.locals;
|
|
21
|
+
const userId = getUserId(request);
|
|
22
|
+
|
|
23
|
+
const channelList = await getChannels(application, userId);
|
|
24
|
+
|
|
25
|
+
response.render("search", {
|
|
26
|
+
title: request.__("microsub.search.title"),
|
|
27
|
+
channels: channelList,
|
|
28
|
+
baseUrl: request.baseUrl,
|
|
29
|
+
readerBaseUrl: request.baseUrl,
|
|
30
|
+
activeView: "channels",
|
|
31
|
+
breadcrumbs: [
|
|
32
|
+
{ text: "Reader", href: request.baseUrl },
|
|
33
|
+
{ text: "Search" },
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Search for feeds from URL - enhanced with validation
|
|
40
|
+
* @param {object} request - Express request
|
|
41
|
+
* @param {object} response - Express response
|
|
42
|
+
* @returns {Promise<void>}
|
|
43
|
+
*/
|
|
44
|
+
export async function searchFeeds(request, response) {
|
|
45
|
+
const { application } = request.app.locals;
|
|
46
|
+
const userId = getUserId(request);
|
|
47
|
+
const { query } = request.body;
|
|
48
|
+
|
|
49
|
+
const channelList = await getChannels(application, userId);
|
|
50
|
+
|
|
51
|
+
let results = [];
|
|
52
|
+
let discoveryError = null;
|
|
53
|
+
|
|
54
|
+
if (query) {
|
|
55
|
+
try {
|
|
56
|
+
// Use enhanced discovery with validation
|
|
57
|
+
results = await discoverAndValidateFeeds(query);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
discoveryError = error.message;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
response.render("search", {
|
|
64
|
+
title: request.__("microsub.search.title"),
|
|
65
|
+
channels: channelList,
|
|
66
|
+
query,
|
|
67
|
+
results,
|
|
68
|
+
discoveryError,
|
|
69
|
+
searched: true,
|
|
70
|
+
baseUrl: request.baseUrl,
|
|
71
|
+
readerBaseUrl: request.baseUrl,
|
|
72
|
+
activeView: "channels",
|
|
73
|
+
breadcrumbs: [
|
|
74
|
+
{ text: "Reader", href: request.baseUrl },
|
|
75
|
+
{ text: "Search" },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Subscribe to a feed from search results - with validation
|
|
82
|
+
* @param {object} request - Express request
|
|
83
|
+
* @param {object} response - Express response
|
|
84
|
+
* @returns {Promise<void>}
|
|
85
|
+
*/
|
|
86
|
+
export async function subscribe(request, response) {
|
|
87
|
+
const { application } = request.app.locals;
|
|
88
|
+
const userId = getUserId(request);
|
|
89
|
+
const { url, channel: channelUid, skipValidation } = request.body;
|
|
90
|
+
|
|
91
|
+
const channelDocument = await getChannel(application, channelUid, userId);
|
|
92
|
+
if (!channelDocument) {
|
|
93
|
+
return response.status(404).render("404");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate feed unless explicitly skipped (for power users)
|
|
97
|
+
if (!skipValidation) {
|
|
98
|
+
const validation = await validateFeedUrl(url);
|
|
99
|
+
|
|
100
|
+
if (!validation.valid) {
|
|
101
|
+
const channelList = await getChannels(application, userId);
|
|
102
|
+
return response.render("search", {
|
|
103
|
+
title: request.__("microsub.search.title"),
|
|
104
|
+
channels: channelList,
|
|
105
|
+
query: url,
|
|
106
|
+
validationError: validation.error,
|
|
107
|
+
baseUrl: request.baseUrl,
|
|
108
|
+
readerBaseUrl: request.baseUrl,
|
|
109
|
+
activeView: "channels",
|
|
110
|
+
breadcrumbs: [
|
|
111
|
+
{ text: "Reader", href: request.baseUrl },
|
|
112
|
+
{ text: "Search" },
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Warn about comments feeds but allow subscription
|
|
118
|
+
if (validation.isCommentsFeed) {
|
|
119
|
+
console.warn(`[Microsub] Subscribing to comments feed: ${url}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Create feed subscription (throws DUPLICATE_FEED if already exists elsewhere)
|
|
124
|
+
try {
|
|
125
|
+
const feed = await createFeed(application, {
|
|
126
|
+
channelId: channelDocument._id,
|
|
127
|
+
url,
|
|
128
|
+
title: undefined,
|
|
129
|
+
photo: undefined,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Trigger immediate fetch in background
|
|
133
|
+
refreshFeedNow(application, feed._id).catch((error) => {
|
|
134
|
+
console.error(`[Microsub] Error fetching new feed ${url}:`, error.message);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
response.redirect(`${request.baseUrl}/channels/${channelUid}/feeds`);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error.code === "DUPLICATE_FEED") {
|
|
140
|
+
const channelList = await getChannels(application, userId);
|
|
141
|
+
return response.render("search", {
|
|
142
|
+
title: request.__("microsub.search.title"),
|
|
143
|
+
channels: channelList,
|
|
144
|
+
query: url,
|
|
145
|
+
validationError: `This feed already exists in channel "${error.channelName}"`,
|
|
146
|
+
baseUrl: request.baseUrl,
|
|
147
|
+
readerBaseUrl: request.baseUrl,
|
|
148
|
+
activeView: "channels",
|
|
149
|
+
breadcrumbs: [
|
|
150
|
+
{ text: "Reader", href: request.baseUrl },
|
|
151
|
+
{ text: "Search" },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|