@rmdes/indiekit-endpoint-rss 1.1.2 → 1.1.3
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/feeds.js +15 -6
- package/lib/publisher.js +30 -16
- package/locales/en.json +5 -0
- package/package.json +1 -1
- package/views/rss.njk +62 -1
package/lib/controllers/feeds.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ObjectId } from "mongodb";
|
|
2
2
|
import { RssClient } from "../rss-client.js";
|
|
3
3
|
import { defaultContent, defaultLinkProperty } from "../jf2-builder.js";
|
|
4
|
-
import { watermarkFor } from "../publisher.js";
|
|
5
4
|
import { formatFeed, isValidUrl, normalizeUrl } from "../utils.js";
|
|
5
|
+
import { pendingQuery, watermarkFor } from "../publisher.js";
|
|
6
6
|
|
|
7
7
|
export const feedsController = {
|
|
8
8
|
/**
|
|
@@ -263,7 +263,17 @@ export const feedsController = {
|
|
|
263
263
|
{ returnDocument: "after" },
|
|
264
264
|
);
|
|
265
265
|
|
|
266
|
+
// Enabling publishing stamps a watermark, so a feed can be correctly
|
|
267
|
+
// configured and still have nothing to publish. Saying so beats silence.
|
|
268
|
+
let eligible;
|
|
269
|
+
if (result.publish?.enabled) {
|
|
270
|
+
eligible = await db
|
|
271
|
+
.collection("rssItems")
|
|
272
|
+
.countDocuments(pendingQuery(result));
|
|
273
|
+
}
|
|
274
|
+
|
|
266
275
|
response.json({
|
|
276
|
+
eligible,
|
|
267
277
|
message:
|
|
268
278
|
enabled === undefined
|
|
269
279
|
? response.locals.__("rss.success.feedUpdated")
|
|
@@ -334,11 +344,10 @@ export const feedsController = {
|
|
|
334
344
|
await feedsCollection.updateOne(
|
|
335
345
|
{ _id: feedId },
|
|
336
346
|
{
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
},
|
|
347
|
+
// The feed's own post status is respected: silently forcing drafts
|
|
348
|
+
// would override a deliberate choice. The guard here is the count
|
|
349
|
+
// the user asked for, and maxPostsPerCycle pacing the catch-up.
|
|
350
|
+
$set: { "publish.since": watermark },
|
|
342
351
|
},
|
|
343
352
|
);
|
|
344
353
|
|
package/lib/publisher.js
CHANGED
|
@@ -102,6 +102,35 @@ export function resolvePublishContext(application = {}, publication = {}) {
|
|
|
102
102
|
|
|
103
103
|
const MAX_ATTEMPTS = 3;
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Query selecting a feed's items that are still waiting to be published.
|
|
107
|
+
*
|
|
108
|
+
* Exported so the dashboard can count them with exactly the same rule the
|
|
109
|
+
* publish loop applies. Two copies of this would drift, and the symptom would
|
|
110
|
+
* be a count that disagrees with what actually publishes.
|
|
111
|
+
* @param {object} feed - Feed document
|
|
112
|
+
* @returns {object} MongoDB query
|
|
113
|
+
*/
|
|
114
|
+
export function pendingQuery(feed) {
|
|
115
|
+
const query = {
|
|
116
|
+
feedId: feed._id,
|
|
117
|
+
postedAt: { $exists: false },
|
|
118
|
+
postSkipped: { $ne: true },
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (feed.publish?.since) {
|
|
122
|
+
// An item with no parsable date would be excluded forever by a bare
|
|
123
|
+
// `pubDate: { $gt: since }`, because null sorts below every Date in BSON.
|
|
124
|
+
// fetchedAt is always set on insert and stands in when pubDate is missing.
|
|
125
|
+
query.$or = [
|
|
126
|
+
{ pubDate: { $gt: feed.publish.since } },
|
|
127
|
+
{ pubDate: null, fetchedAt: { $gt: feed.publish.since } },
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return query;
|
|
132
|
+
}
|
|
133
|
+
|
|
105
134
|
/**
|
|
106
135
|
* Publish the pending items of one feed.
|
|
107
136
|
*
|
|
@@ -131,22 +160,7 @@ export async function publishPending(feed, itemsCollection, options) {
|
|
|
131
160
|
mintImpl = mintToken,
|
|
132
161
|
} = options;
|
|
133
162
|
|
|
134
|
-
const query =
|
|
135
|
-
feedId: feed._id,
|
|
136
|
-
postedAt: { $exists: false },
|
|
137
|
-
postSkipped: { $ne: true },
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
if (feed.publish.since) {
|
|
141
|
-
// An item with no parsable date would be excluded forever by a bare
|
|
142
|
-
// `pubDate: { $gt: since }`, because null sorts below every Date in BSON —
|
|
143
|
-
// a silent permanent no-op. fetchedAt is always set on insert and is
|
|
144
|
-
// monotonic with cache entry, so it stands in when pubDate is missing.
|
|
145
|
-
query.$or = [
|
|
146
|
-
{ pubDate: { $gt: feed.publish.since } },
|
|
147
|
-
{ pubDate: null, fetchedAt: { $gt: feed.publish.since } },
|
|
148
|
-
];
|
|
149
|
-
}
|
|
163
|
+
const query = pendingQuery(feed);
|
|
150
164
|
|
|
151
165
|
const items = await itemsCollection
|
|
152
166
|
.find(query)
|
package/locales/en.json
CHANGED
|
@@ -68,6 +68,11 @@
|
|
|
68
68
|
"title": "Feed settings",
|
|
69
69
|
"url": "Feed URL",
|
|
70
70
|
"urlHint": "Correcting this keeps the cached items; removing and re-adding the feed would lose them."
|
|
71
|
+
},
|
|
72
|
+
"backfill": {
|
|
73
|
+
"label": "Publish existing items",
|
|
74
|
+
"hint": "How many of the newest cached items to publish. They are created with this feed's post status, a few per sync cycle.",
|
|
75
|
+
"submit": "Publish existing items"
|
|
71
76
|
}
|
|
72
77
|
}
|
|
73
78
|
}
|
package/package.json
CHANGED
package/views/rss.njk
CHANGED
|
@@ -180,6 +180,24 @@
|
|
|
180
180
|
text: __("rss.publish.save")
|
|
181
181
|
}) }}
|
|
182
182
|
</form>
|
|
183
|
+
|
|
184
|
+
{% if feed.publish.enabled %}
|
|
185
|
+
<form class="rss-backfill" data-backfill-feed="{{ feed.id }}">
|
|
186
|
+
{{ input({
|
|
187
|
+
id: "backfill-last-" + feed.id,
|
|
188
|
+
name: "last",
|
|
189
|
+
type: "number",
|
|
190
|
+
label: __("rss.backfill.label"),
|
|
191
|
+
hint: __("rss.backfill.hint"),
|
|
192
|
+
value: "10"
|
|
193
|
+
}) }}
|
|
194
|
+
{{ button({
|
|
195
|
+
classes: "button--secondary",
|
|
196
|
+
type: "submit",
|
|
197
|
+
text: __("rss.backfill.submit")
|
|
198
|
+
}) }}
|
|
199
|
+
</form>
|
|
200
|
+
{% endif %}
|
|
183
201
|
{% endcall %}
|
|
184
202
|
</div>
|
|
185
203
|
<div class="rss-feed-actions">
|
|
@@ -338,6 +356,38 @@
|
|
|
338
356
|
}
|
|
339
357
|
});
|
|
340
358
|
|
|
359
|
+
// Handle backfill
|
|
360
|
+
document.querySelectorAll('[data-backfill-feed]').forEach(form => {
|
|
361
|
+
form.addEventListener('submit', async (e) => {
|
|
362
|
+
e.preventDefault();
|
|
363
|
+
const feedId = e.target.dataset.backfillFeed;
|
|
364
|
+
const last = Number(new FormData(e.target).get('last'));
|
|
365
|
+
|
|
366
|
+
if (!last || last < 1) {
|
|
367
|
+
alert('Enter how many recent items to publish.');
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const response = await fetch(`{{ mountPath }}/api/feeds/${feedId}/backfill`, {
|
|
373
|
+
method: 'POST',
|
|
374
|
+
headers: { 'Content-Type': 'application/json' },
|
|
375
|
+
body: JSON.stringify({ last })
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const body = await response.json();
|
|
379
|
+
if (response.ok) {
|
|
380
|
+
alert(body.message || 'Backfill queued.');
|
|
381
|
+
location.reload();
|
|
382
|
+
} else {
|
|
383
|
+
alert(body.error || 'Failed to queue backfill');
|
|
384
|
+
}
|
|
385
|
+
} catch (err) {
|
|
386
|
+
alert('Failed to queue backfill: ' + err.message);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
341
391
|
// Handle delete feed
|
|
342
392
|
document.querySelectorAll('[data-delete-feed]').forEach(btn => {
|
|
343
393
|
btn.addEventListener('click', async (e) => {
|
|
@@ -386,10 +436,21 @@
|
|
|
386
436
|
body: JSON.stringify({ url: data.get('url'), publish })
|
|
387
437
|
});
|
|
388
438
|
|
|
439
|
+
const body = await response.json();
|
|
440
|
+
|
|
389
441
|
if (response.ok) {
|
|
442
|
+
// A feed can be correctly configured and still have nothing to
|
|
443
|
+
// publish: the watermark only lets through items dated after the
|
|
444
|
+
// moment publishing was switched on.
|
|
445
|
+
if (body.eligible === 0) {
|
|
446
|
+
alert(
|
|
447
|
+
'Saved. No items are waiting: only items published after you ' +
|
|
448
|
+
'enabled this will post. Use "Publish existing items" to ' +
|
|
449
|
+
'include the ones already cached.'
|
|
450
|
+
);
|
|
451
|
+
}
|
|
390
452
|
location.reload();
|
|
391
453
|
} else {
|
|
392
|
-
const body = await response.json();
|
|
393
454
|
alert(body.error || 'Failed to save publish settings');
|
|
394
455
|
}
|
|
395
456
|
} catch (err) {
|