feedkeeper 0.35.4 → 0.35.5
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/package.json +3 -3
- package/server/dist/api/auth.js +1 -1
- package/server/dist/api/feeds.js +1 -1
- package/server/dist/api/folders.js +11 -8
- package/server/dist/api/onboarding.js +2 -1
- package/server/dist/api/v1/resources.js +14 -19
- package/server/dist/auth/middleware.js +2 -2
- package/server/dist/db/migrations/0041_sync_feed_metadata.sql +16 -0
- package/server/dist/feeds/archive.js +3 -2
- package/server/dist/feeds/content/noise/lazyImages.js +7 -6
- package/server/dist/feeds/extractor.js +1 -1
- package/server/dist/feeds/feedIcon.js +4 -2
- package/server/dist/feeds/imageSources.js +69 -0
- package/server/dist/feeds/inbox.js +4 -9
- package/server/dist/feeds/opml.js +122 -27
- package/server/dist/feeds/repository.js +19 -7
- package/server/dist/mcp/server.js +10 -0
- package/server/dist/server.js +1 -1
- package/server/dist/sync/changeLog.js +1 -0
- package/server/package.json +1 -1
- package/web/dist/assets/index-CilgZLF5.js +32 -0
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/index-CYi9R_Ds.js +0 -32
|
@@ -16,6 +16,14 @@ export function createFeed(url, pollIntervalMinutes) {
|
|
|
16
16
|
.run(url, pollIntervalMinutes);
|
|
17
17
|
return findFeedById(Number(result.lastInsertRowid));
|
|
18
18
|
}
|
|
19
|
+
export function seedFeedMetadata(feedId, data) {
|
|
20
|
+
if (!data.title && !data.siteUrl)
|
|
21
|
+
return;
|
|
22
|
+
db.prepare(`UPDATE feeds SET
|
|
23
|
+
title = COALESCE(NULLIF(TRIM(title), ''), ?),
|
|
24
|
+
site_url = COALESCE(NULLIF(TRIM(site_url), ''), ?)
|
|
25
|
+
WHERE id = ?`).run(data.title?.trim() || null, data.siteUrl?.trim() || null, feedId);
|
|
26
|
+
}
|
|
19
27
|
export function subscribe(userId, feedId, label, folderId) {
|
|
20
28
|
const maxPosRow = db
|
|
21
29
|
.prepare("SELECT MAX(position) AS max_pos FROM subscriptions WHERE user_id = ?")
|
|
@@ -170,7 +178,7 @@ export function upsertItems(feedId, items) {
|
|
|
170
178
|
refreshLead(Number(result.lastInsertRowid));
|
|
171
179
|
}
|
|
172
180
|
else {
|
|
173
|
-
const previous = db.prepare("SELECT id, title, link, image_url, content_snippet, content_html, full_content_html, lead_version FROM items WHERE feed_id = ? AND guid = ?").get(feedId, row.guid);
|
|
181
|
+
const previous = db.prepare("SELECT id, title, link, image_url, content_snippet, content_html, full_content_html, published_at, author, lead_version FROM items WHERE feed_id = ? AND guid = ?").get(feedId, row.guid);
|
|
174
182
|
const next = row.contentHtml;
|
|
175
183
|
// Some publishers replace complete feed bodies with short teasers on a later poll.
|
|
176
184
|
const retained = previous?.content_html && next && next.length < previous.content_html.length * 0.9 &&
|
|
@@ -185,9 +193,9 @@ export function upsertItems(feedId, items) {
|
|
|
185
193
|
(row.link !== undefined && row.link !== previous.link) ||
|
|
186
194
|
(!keptPrevious && row.contentSnippet !== undefined && row.contentSnippet !== previous.content_snippet) ||
|
|
187
195
|
(retained !== null && retained !== previous.content_html) ||
|
|
188
|
-
(row.publishedAt !== undefined && row.publishedAt !== null) ||
|
|
196
|
+
(row.publishedAt !== undefined && row.publishedAt !== null && row.publishedAt !== previous.published_at) ||
|
|
189
197
|
(nextImage !== previous.image_url) ||
|
|
190
|
-
(row.author !== undefined);
|
|
198
|
+
(row.author !== undefined && row.author !== null && row.author !== previous.author);
|
|
191
199
|
if (changed) {
|
|
192
200
|
update.run(row.title ?? null, row.link ?? null, nextSnippet, retained, row.publishedAt ?? null, nextImage, row.author ?? null, feedId, row.guid);
|
|
193
201
|
const leadChanged = (retained !== null && retained !== previous.content_html) ||
|
|
@@ -210,15 +218,17 @@ export function upsertItems(feedId, items) {
|
|
|
210
218
|
return insertMany(items);
|
|
211
219
|
}
|
|
212
220
|
export function listItemsForUser(userId, opts = {}) {
|
|
213
|
-
// Saved articles stay visible
|
|
221
|
+
// Saved and annotated articles stay visible after unsubscribing.
|
|
214
222
|
const conditions = [];
|
|
223
|
+
const params = [];
|
|
215
224
|
if (opts.bookmarkedOnly) {
|
|
216
225
|
conditions.push("b.item_id IS NOT NULL");
|
|
217
226
|
}
|
|
218
227
|
else {
|
|
219
|
-
conditions.push(
|
|
228
|
+
conditions.push(`(s.id IS NOT NULL OR b.item_id IS NOT NULL OR
|
|
229
|
+
EXISTS (SELECT 1 FROM item_notes access_note WHERE access_note.item_id = i.id AND access_note.user_id = ?))`);
|
|
230
|
+
params.push(userId);
|
|
220
231
|
}
|
|
221
|
-
const params = [];
|
|
222
232
|
// Keep the stored item available to direct reads and sync; deliberate saves/notes override stream filtering.
|
|
223
233
|
conditions.push(`(i.is_empty_gallery = 0 OR b.item_id IS NOT NULL OR
|
|
224
234
|
EXISTS (SELECT 1 FROM item_notes gallery_note WHERE gallery_note.item_id = i.id AND gallery_note.user_id = ?))`);
|
|
@@ -410,7 +420,9 @@ export function findFolderById(userId, id) {
|
|
|
410
420
|
}
|
|
411
421
|
export function createFolder(userId, name, iconSymbol) {
|
|
412
422
|
const trimmed = name.trim();
|
|
413
|
-
db.prepare(
|
|
423
|
+
db.prepare(`INSERT INTO folders (user_id, name, icon_symbol) VALUES (?, ?, ?)
|
|
424
|
+
ON CONFLICT (user_id, name) DO UPDATE SET icon_symbol = excluded.icon_symbol
|
|
425
|
+
WHERE ? AND folders.icon_symbol IS NOT excluded.icon_symbol`).run(userId, trimmed, iconSymbol ?? null, Number(iconSymbol !== undefined));
|
|
414
426
|
return findFolderByName(userId, trimmed);
|
|
415
427
|
}
|
|
416
428
|
export function updateFolder(userId, id, updates) {
|
|
@@ -68,6 +68,8 @@ export function createMcpServerForUser(userId, scope) {
|
|
|
68
68
|
folderId: z.number().int().positive().optional().describe("Optional folder/category ID"),
|
|
69
69
|
},
|
|
70
70
|
}, async ({ url, label, folderId }) => {
|
|
71
|
+
if (!hasUserCapability(userId, "sync"))
|
|
72
|
+
return { ...textResult(capabilityDenial(userId, "sync")), isError: true };
|
|
71
73
|
try {
|
|
72
74
|
const subscription = await subscribeToFeed(userId, url, label ?? null, folderId ?? null);
|
|
73
75
|
return textResult(subscription);
|
|
@@ -94,6 +96,8 @@ export function createMcpServerForUser(userId, scope) {
|
|
|
94
96
|
url: mcpUrlSchema.describe("The website URL to discover feeds from"),
|
|
95
97
|
},
|
|
96
98
|
}, async ({ url }) => {
|
|
99
|
+
if (!hasUserCapability(userId, "sync"))
|
|
100
|
+
return { ...textResult(capabilityDenial(userId, "sync")), isError: true };
|
|
97
101
|
try {
|
|
98
102
|
const feeds = await discoverFeeds(url);
|
|
99
103
|
return textResult({ feeds });
|
|
@@ -193,6 +197,8 @@ export function createMcpServerForUser(userId, scope) {
|
|
|
193
197
|
badge: z.boolean().optional().describe("Count this feed's unread articles in the number on the app icon"),
|
|
194
198
|
},
|
|
195
199
|
}, async ({ feedId, url, label, pollIntervalMinutes, fullText, notify, badge }) => {
|
|
200
|
+
if (url && !hasUserCapability(userId, "sync"))
|
|
201
|
+
return { ...textResult(capabilityDenial(userId, "sync")), isError: true };
|
|
196
202
|
try {
|
|
197
203
|
let targetFeedId = feedId;
|
|
198
204
|
let previousItemsKept;
|
|
@@ -429,6 +435,8 @@ export function createMcpServerForUser(userId, scope) {
|
|
|
429
435
|
opml: z.string().min(1).describe("The OPML XML content to import"),
|
|
430
436
|
},
|
|
431
437
|
}, async ({ opml }) => {
|
|
438
|
+
if (!hasUserCapability(userId, "sync"))
|
|
439
|
+
return { ...textResult(capabilityDenial(userId, "sync")), isError: true };
|
|
432
440
|
try {
|
|
433
441
|
const result = await importOpmlFeeds(userId, opml);
|
|
434
442
|
return textResult(result);
|
|
@@ -445,6 +453,8 @@ export function createMcpServerForUser(userId, scope) {
|
|
|
445
453
|
feedId: z.number().int().positive().describe("The ID of the feed to refresh"),
|
|
446
454
|
},
|
|
447
455
|
}, async ({ feedId }) => {
|
|
456
|
+
if (!hasUserCapability(userId, "sync"))
|
|
457
|
+
return { ...textResult(capabilityDenial(userId, "sync")), isError: true };
|
|
448
458
|
const feed = findFeedById(feedId);
|
|
449
459
|
if (!feed || !isUserSubscribed(userId, feedId)) {
|
|
450
460
|
return errorResult("Feed not found or not subscribed");
|
package/server/dist/server.js
CHANGED
|
@@ -62,7 +62,7 @@ app.use(cors({
|
|
|
62
62
|
credentials: true,
|
|
63
63
|
}));
|
|
64
64
|
app.use(express.json({ limit: "1mb" }));
|
|
65
|
-
app.use(express.text({ type: ["application/xml", "text/xml", "text/plain"], limit: "5mb" }));
|
|
65
|
+
app.use(express.text({ type: ["application/xml", "text/xml", "text/x-opml", "application/opml+xml", "text/plain"], limit: "5mb" }));
|
|
66
66
|
app.use(cookieSession({
|
|
67
67
|
name: "feedkeeper.sid",
|
|
68
68
|
secret: config.sessionSecret,
|
|
@@ -20,6 +20,7 @@ export function serializeSubscription(subscription) {
|
|
|
20
20
|
notify: Boolean(subscription.notify),
|
|
21
21
|
badge: Boolean(subscription.badge),
|
|
22
22
|
isInbox: Boolean(subscription.is_system_inbox),
|
|
23
|
+
lastError: subscription.last_error,
|
|
23
24
|
iconHash: subscription.icon_hash ?? null,
|
|
24
25
|
iconUrl: subscription.icon_hash
|
|
25
26
|
? `/api/v1/subscriptions/${subscription.id}/icon?v=${subscription.icon_hash}`
|