backend-manager 5.8.2 → 5.9.0
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/CHANGELOG.md +35 -0
- package/CLAUDE.md +1 -1
- package/PROGRESS.md +4 -4
- package/docs/admin-post-route.md +1 -1
- package/docs/ghostii.md +89 -61
- package/docs/marketing-campaigns.md +2 -2
- package/package.json +1 -1
- package/src/manager/events/cron/daily/blog-auto-publisher.js +680 -0
- package/src/manager/libraries/content/feed-parser.js +1 -1
- package/src/manager/libraries/content/ghostii.js +3 -3
- package/src/manager/libraries/email/generators/lib/structure.js +5 -1
- package/src/manager/libraries/email/generators/newsletter.js +47 -46
- package/src/manager/routes/brand/get.js +1 -1
- package/templates/backend-manager-config.json +66 -70
- package/test/helpers/content/blog-auto-publisher.js +484 -0
- package/test/helpers/content/ghostii-feed-integration.js +33 -28
- package/test/helpers/content/ghostii-write-article.js +1 -1
- package/test/routes/test/usage.js +1 -1
- package/src/manager/events/cron/daily/ghostii-auto-publisher.js +0 -511
- package/test/helpers/content/ghostii-auto-publisher.js +0 -342
|
@@ -1,511 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ghostii Auto Publisher — daily cron job.
|
|
3
|
-
*
|
|
4
|
-
* Generates and publishes blog posts using the Ghostii AI article engine.
|
|
5
|
-
* Iterates `config.ghostii[]` entries, each defining a source pool (generic
|
|
6
|
-
* topics, URLs, text prompts, or RSS/Atom feeds) and per-entry overrides for
|
|
7
|
-
* the Ghostii API params.
|
|
8
|
-
*
|
|
9
|
-
* Source types:
|
|
10
|
-
* '$app' — generic brand-topic generation
|
|
11
|
-
* '$feed:<url>' — RSS/Atom/JSON feed: pick one unprocessed article per run
|
|
12
|
-
* 'https://...' — fetch URL content as prompt seed
|
|
13
|
-
* '<text>' — use directly as prompt seed
|
|
14
|
-
*
|
|
15
|
-
* Feed items are tracked in Firestore (`ghostii-feed-items`) so the same
|
|
16
|
-
* article is never processed twice. When a feed is unreachable or exhausted,
|
|
17
|
-
* the entry falls back to $app behavior.
|
|
18
|
-
*/
|
|
19
|
-
const crypto = require('crypto');
|
|
20
|
-
const fetch = require('wonderful-fetch');
|
|
21
|
-
const powertools = require('node-powertools');
|
|
22
|
-
const moment = require('moment');
|
|
23
|
-
const JSON5 = require('json5');
|
|
24
|
-
|
|
25
|
-
const { writeArticle, publishArticle } = require('../../../libraries/content/ghostii.js');
|
|
26
|
-
const { parseFeed, extractArticleContent } = require('../../../libraries/content/feed-parser.js');
|
|
27
|
-
|
|
28
|
-
const PROMPT = `
|
|
29
|
-
Company: {brand.brand.name}: {brand.brand.description}
|
|
30
|
-
Date: {date}
|
|
31
|
-
Instructions: {prompt}
|
|
32
|
-
|
|
33
|
-
Use the following information to find a topic for our company blog (it can be about our company OR any topic that would be relevant to our website and business BUT not about a competitor):
|
|
34
|
-
{suggestion}
|
|
35
|
-
`;
|
|
36
|
-
|
|
37
|
-
const PROMPT_SOURCE = `
|
|
38
|
-
Company: {brand.brand.name}: {brand.brand.description}
|
|
39
|
-
Date: {date}
|
|
40
|
-
Instructions: {prompt}
|
|
41
|
-
|
|
42
|
-
Write an original article inspired by and referencing this source material.
|
|
43
|
-
Do NOT copy the source — use it as context and inspiration for a unique take.
|
|
44
|
-
Source title: {sourceTitle}
|
|
45
|
-
`;
|
|
46
|
-
|
|
47
|
-
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36';
|
|
48
|
-
const FEED_PREFIX = '$feed:';
|
|
49
|
-
|
|
50
|
-
// State
|
|
51
|
-
let postId;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Ghostii Auto Publisher cron job
|
|
55
|
-
*
|
|
56
|
-
* Automatically generates and publishes blog posts using Ghostii AI.
|
|
57
|
-
*/
|
|
58
|
-
module.exports = async ({ Manager, assistant, context, libraries }) => {
|
|
59
|
-
// Log
|
|
60
|
-
assistant.log('Starting...');
|
|
61
|
-
|
|
62
|
-
// Set post ID
|
|
63
|
-
postId = moment().unix();
|
|
64
|
-
|
|
65
|
-
// Build brand config from local config
|
|
66
|
-
const brandConfig = buildBrandConfig(Manager.config);
|
|
67
|
-
|
|
68
|
-
// Log
|
|
69
|
-
assistant.log('Brand config', brandConfig);
|
|
70
|
-
|
|
71
|
-
// Get settings
|
|
72
|
-
const settingsArray = powertools.arrayify(Manager.config.ghostii);
|
|
73
|
-
|
|
74
|
-
// Get admin for Firestore tracking
|
|
75
|
-
const { admin } = libraries;
|
|
76
|
-
|
|
77
|
-
// Loop through each item
|
|
78
|
-
for (const settings of settingsArray) {
|
|
79
|
-
// Fix settings
|
|
80
|
-
settings.articles = settings.articles || 0;
|
|
81
|
-
settings.sources = randomize(settings.sources || []);
|
|
82
|
-
settings.links = randomize(settings.links || []);
|
|
83
|
-
settings.prompt = settings.prompt || '';
|
|
84
|
-
settings.chance = settings.chance || 1.0;
|
|
85
|
-
settings.author = settings.author || undefined;
|
|
86
|
-
settings.postPath = settings.postPath || 'ghostii';
|
|
87
|
-
settings.overrides = settings.overrides || {};
|
|
88
|
-
|
|
89
|
-
// Resolve brand data for this ghostii item
|
|
90
|
-
if (settings.brand && settings.brandUrl) {
|
|
91
|
-
// Cross-brand: fetch from the other project's /brand endpoint
|
|
92
|
-
settings.brand = await fetchRemoteBrand(settings.brandUrl).catch((e) => e);
|
|
93
|
-
|
|
94
|
-
if (settings.brand instanceof Error) {
|
|
95
|
-
assistant.error('Error fetching remote brand data', settings.brand);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
// Same-brand: use local config
|
|
100
|
-
settings.brand = brandConfig;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Log
|
|
104
|
-
assistant.log(`Settings (brand=${settings.brand.brand.id})`, settings);
|
|
105
|
-
|
|
106
|
-
// Quit if articles are disabled
|
|
107
|
-
if (!settings.articles || !settings.sources.length) {
|
|
108
|
-
assistant.log('Quitting because articles are disabled');
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Quit if the chance is not met
|
|
113
|
-
const chance = Math.random();
|
|
114
|
-
if (chance > settings.chance) {
|
|
115
|
-
assistant.log(`Quitting because the chance is not met (${chance} <= ${settings.chance})`);
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Harvest articles
|
|
120
|
-
const result = await harvest(assistant, settings, admin).catch((e) => e);
|
|
121
|
-
if (result instanceof Error) {
|
|
122
|
-
throw result;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Log
|
|
126
|
-
assistant.log('Finished!', result);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Build brand config from Manager.config (same shape as /brand endpoint response)
|
|
132
|
-
*/
|
|
133
|
-
function buildBrandConfig(config) {
|
|
134
|
-
const { buildPublicConfig } = require(require('path').join(__dirname, '..', '..', '..', 'routes', 'brand', 'get.js'));
|
|
135
|
-
|
|
136
|
-
return buildPublicConfig(config);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Fetch brand data from a remote BEM project's /brand endpoint
|
|
141
|
-
*/
|
|
142
|
-
function fetchRemoteBrand(brandUrl) {
|
|
143
|
-
return fetch(`${brandUrl}/backend-manager/brand`, {
|
|
144
|
-
timeout: 120000,
|
|
145
|
-
tries: 3,
|
|
146
|
-
response: 'json',
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function harvest(assistant, settings, admin) {
|
|
151
|
-
const date = moment().format('MMMM YYYY');
|
|
152
|
-
|
|
153
|
-
// Log
|
|
154
|
-
assistant.log(`harvest(): Starting ${settings.brand.brand.id}...`);
|
|
155
|
-
|
|
156
|
-
// Process the number of sources in the settings
|
|
157
|
-
for (let index = 0; index < settings.articles; index++) {
|
|
158
|
-
const source = powertools.random(settings.sources);
|
|
159
|
-
|
|
160
|
-
// Log
|
|
161
|
-
assistant.log(`harvest(): Processing ${index + 1}/${settings.articles}`, source);
|
|
162
|
-
|
|
163
|
-
// Resolve the source into a prompt description + optional sourceContent
|
|
164
|
-
const resolved = await resolveSource(assistant, source, settings, admin).catch((e) => e);
|
|
165
|
-
if (resolved instanceof Error) {
|
|
166
|
-
assistant.error('harvest(): Error resolving source', resolved);
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Log
|
|
171
|
-
assistant.log('harvest(): Resolved source', resolved);
|
|
172
|
-
|
|
173
|
-
// Request to Ghostii
|
|
174
|
-
const article = await writeArticle({
|
|
175
|
-
brand: settings.brand,
|
|
176
|
-
description: resolved.description,
|
|
177
|
-
links: settings.links,
|
|
178
|
-
sourceContent: resolved.sourceContent || '',
|
|
179
|
-
overrides: settings.overrides,
|
|
180
|
-
}).catch((e) => e);
|
|
181
|
-
if (article instanceof Error) {
|
|
182
|
-
assistant.error('harvest(): Error requesting Ghostii', article);
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Log
|
|
187
|
-
assistant.log('harvest(): Article', article);
|
|
188
|
-
|
|
189
|
-
// Upload post to blog
|
|
190
|
-
const uploadedPost = await publishArticle(assistant, {
|
|
191
|
-
brand: settings.brand,
|
|
192
|
-
article,
|
|
193
|
-
id: postId++,
|
|
194
|
-
author: settings.author,
|
|
195
|
-
postPath: settings.postPath,
|
|
196
|
-
}).catch((e) => e);
|
|
197
|
-
if (uploadedPost instanceof Error) {
|
|
198
|
-
assistant.error('harvest(): Error uploading post to blog', uploadedPost);
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Log
|
|
203
|
-
assistant.log('harvest(): Uploaded post', uploadedPost);
|
|
204
|
-
|
|
205
|
-
// Track feed item in Firestore (if this was a feed source)
|
|
206
|
-
if (resolved.feedItem && admin) {
|
|
207
|
-
await trackFeedItem(admin, {
|
|
208
|
-
feedUrl: resolved.feedUrl,
|
|
209
|
-
item: resolved.feedItem,
|
|
210
|
-
brandId: settings.brand.brand.id,
|
|
211
|
-
postUrl: uploadedPost.url,
|
|
212
|
-
postSlug: uploadedPost.slug,
|
|
213
|
-
}).catch((e) => {
|
|
214
|
-
assistant.error('harvest(): Error tracking feed item (non-fatal)', e);
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Resolve a source entry into { description, sourceContent, feedItem?, feedUrl? }.
|
|
222
|
-
*
|
|
223
|
-
* Source types:
|
|
224
|
-
* '$app' — generic topic prompt, no sourceContent
|
|
225
|
-
* '$feed:<url>' — RSS/Atom feed: pick unprocessed article, extract content
|
|
226
|
-
* URL — fetch page content as prompt seed
|
|
227
|
-
* text — use directly as prompt seed
|
|
228
|
-
*
|
|
229
|
-
* Feed failures fall back to $app behavior.
|
|
230
|
-
*/
|
|
231
|
-
async function resolveSource(assistant, source, settings, admin) {
|
|
232
|
-
const date = moment().format('MMMM YYYY');
|
|
233
|
-
|
|
234
|
-
// --- $feed: source ---
|
|
235
|
-
if (typeof source === 'string' && source.startsWith(FEED_PREFIX)) {
|
|
236
|
-
const feedUrl = source.slice(FEED_PREFIX.length);
|
|
237
|
-
assistant.log(`resolveSource(): Processing feed: ${feedUrl}`);
|
|
238
|
-
|
|
239
|
-
const feedResult = await processFeedSource(assistant, feedUrl, settings.brand.brand.id, admin).catch((e) => e);
|
|
240
|
-
|
|
241
|
-
if (feedResult instanceof Error || !feedResult) {
|
|
242
|
-
assistant.log('resolveSource(): Feed failed or exhausted, falling back to $app');
|
|
243
|
-
return resolveSource(assistant, '$app', settings, admin);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const description = powertools.template(PROMPT_SOURCE, {
|
|
247
|
-
...settings,
|
|
248
|
-
prompt: settings.prompt,
|
|
249
|
-
date: date,
|
|
250
|
-
sourceTitle: feedResult.item.title,
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
description,
|
|
255
|
-
sourceContent: feedResult.content || feedResult.item.summary || '',
|
|
256
|
-
feedItem: feedResult.item,
|
|
257
|
-
feedUrl: feedUrl,
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// --- $app source ---
|
|
262
|
-
if (source === '$app') {
|
|
263
|
-
const suggestion = 'Write an article about any topic that would be relevant to our website and business (it does not have to be about our company, but it can be)';
|
|
264
|
-
const description = powertools.template(PROMPT, {
|
|
265
|
-
...settings,
|
|
266
|
-
prompt: settings.prompt,
|
|
267
|
-
date: date,
|
|
268
|
-
suggestion: suggestion,
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
return { description, sourceContent: '' };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// --- URL source ---
|
|
275
|
-
if (isURL(source)) {
|
|
276
|
-
const suggestion = await getURLContent(source).catch((e) => e);
|
|
277
|
-
|
|
278
|
-
if (suggestion instanceof Error) {
|
|
279
|
-
assistant.error(`resolveSource(): Error fetching URL ${source}`, suggestion);
|
|
280
|
-
return resolveSource(assistant, '$app', settings, admin);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const description = powertools.template(PROMPT, {
|
|
284
|
-
...settings,
|
|
285
|
-
prompt: settings.prompt,
|
|
286
|
-
date: date,
|
|
287
|
-
suggestion: suggestion,
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
return { description, sourceContent: '' };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// --- Text source ---
|
|
294
|
-
const description = powertools.template(PROMPT, {
|
|
295
|
-
...settings,
|
|
296
|
-
prompt: settings.prompt,
|
|
297
|
-
date: date,
|
|
298
|
-
suggestion: source,
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
return { description, sourceContent: '' };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Process a $feed: source — fetch, parse, select unprocessed item, extract content.
|
|
306
|
-
*
|
|
307
|
-
* @param {object} assistant - BEM assistant
|
|
308
|
-
* @param {string} feedUrl - RSS/Atom/JSON feed URL
|
|
309
|
-
* @param {string} brandId - Brand ID for tracking
|
|
310
|
-
* @param {object} admin - Firebase Admin SDK
|
|
311
|
-
* @returns {Promise<{ item: FeedItem, content: string }|null>} Selected item + extracted content, or null
|
|
312
|
-
*/
|
|
313
|
-
async function processFeedSource(assistant, feedUrl, brandId, admin) {
|
|
314
|
-
// Fetch the feed
|
|
315
|
-
const feedText = await fetch(feedUrl, {
|
|
316
|
-
timeout: 30000,
|
|
317
|
-
tries: 2,
|
|
318
|
-
response: 'text',
|
|
319
|
-
headers: { 'User-Agent': USER_AGENT },
|
|
320
|
-
}).catch((e) => e);
|
|
321
|
-
|
|
322
|
-
if (feedText instanceof Error) {
|
|
323
|
-
assistant.error(`processFeedSource(): Failed to fetch feed: ${feedUrl}`, feedText);
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Parse the feed
|
|
328
|
-
const { items } = parseFeed(feedText);
|
|
329
|
-
if (!items.length) {
|
|
330
|
-
assistant.log(`processFeedSource(): No items in feed: ${feedUrl}`);
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
assistant.log(`processFeedSource(): Parsed ${items.length} items from feed`);
|
|
335
|
-
|
|
336
|
-
// Get already-processed item IDs from Firestore
|
|
337
|
-
const processedIds = await getProcessedItemIds(admin, feedUrl).catch((e) => {
|
|
338
|
-
assistant.error('processFeedSource(): Error querying tracked items (continuing)', e);
|
|
339
|
-
return new Set();
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
// Filter to unprocessed items
|
|
343
|
-
const unprocessed = items.filter((item) => !processedIds.has(item.id) && !processedIds.has(item.url));
|
|
344
|
-
if (!unprocessed.length) {
|
|
345
|
-
assistant.log(`processFeedSource(): All ${items.length} items already processed for: ${feedUrl}`);
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
assistant.log(`processFeedSource(): ${unprocessed.length} unprocessed items available`);
|
|
350
|
-
|
|
351
|
-
// Pick the first (newest) unprocessed item
|
|
352
|
-
const item = unprocessed[0];
|
|
353
|
-
|
|
354
|
-
// Extract full article content from the item URL
|
|
355
|
-
let content = '';
|
|
356
|
-
if (item.url) {
|
|
357
|
-
content = await extractArticleContent(item.url).catch((e) => {
|
|
358
|
-
assistant.error(`processFeedSource(): Failed to extract content from ${item.url} (using summary)`, e);
|
|
359
|
-
return '';
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Fall back to inline feed content or summary
|
|
364
|
-
if (!content || content.length < 100) {
|
|
365
|
-
content = item.content || item.summary || '';
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return { item, content };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* Query Firestore for already-processed feed item IDs.
|
|
373
|
-
*
|
|
374
|
-
* @param {object} admin - Firebase Admin SDK
|
|
375
|
-
* @param {string} feedUrl - The feed URL to query
|
|
376
|
-
* @returns {Promise<Set<string>>} Set of processed item IDs
|
|
377
|
-
*/
|
|
378
|
-
async function getProcessedItemIds(admin, feedUrl) {
|
|
379
|
-
if (!admin) {
|
|
380
|
-
return new Set();
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const snapshot = await admin.firestore()
|
|
384
|
-
.collection('ghostii-feed-items')
|
|
385
|
-
.where('feedUrl', '==', feedUrl)
|
|
386
|
-
.select('itemId', 'itemUrl')
|
|
387
|
-
.get();
|
|
388
|
-
|
|
389
|
-
const ids = new Set();
|
|
390
|
-
snapshot.docs.forEach((doc) => {
|
|
391
|
-
const data = doc.data();
|
|
392
|
-
if (data.itemId) {
|
|
393
|
-
ids.add(data.itemId);
|
|
394
|
-
}
|
|
395
|
-
if (data.itemUrl) {
|
|
396
|
-
ids.add(data.itemUrl);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
return ids;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Write a tracking doc for a processed feed item.
|
|
405
|
-
*
|
|
406
|
-
* @param {object} admin - Firebase Admin SDK
|
|
407
|
-
* @param {object} args
|
|
408
|
-
* @param {string} args.feedUrl - The source feed URL
|
|
409
|
-
* @param {object} args.item - The processed feed item
|
|
410
|
-
* @param {string} args.brandId - Brand that processed it
|
|
411
|
-
* @param {string} args.postUrl - Published blog post URL
|
|
412
|
-
* @param {string} args.postSlug - Published blog post slug
|
|
413
|
-
*/
|
|
414
|
-
async function trackFeedItem(admin, { feedUrl, item, brandId, postUrl, postSlug }) {
|
|
415
|
-
const docId = feedItemHash(feedUrl, item.id || item.url);
|
|
416
|
-
|
|
417
|
-
await admin.firestore().doc(`ghostii-feed-items/${docId}`).set({
|
|
418
|
-
feedUrl: feedUrl,
|
|
419
|
-
itemId: item.id || item.url,
|
|
420
|
-
itemUrl: item.url,
|
|
421
|
-
itemTitle: item.title,
|
|
422
|
-
processedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
423
|
-
brandId: brandId,
|
|
424
|
-
postUrl: postUrl || null,
|
|
425
|
-
postSlug: postSlug || null,
|
|
426
|
-
metadata: {
|
|
427
|
-
created: admin.firestore.FieldValue.serverTimestamp(),
|
|
428
|
-
},
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Deterministic hash for a feed item — used as the Firestore doc ID.
|
|
434
|
-
*/
|
|
435
|
-
function feedItemHash(feedUrl, itemId) {
|
|
436
|
-
return crypto.createHash('sha256').update(`${feedUrl}::${itemId}`).digest('hex').slice(0, 20);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function getURLContent(url) {
|
|
440
|
-
return fetch(url, {
|
|
441
|
-
timeout: 120000,
|
|
442
|
-
tries: 3,
|
|
443
|
-
response: 'raw',
|
|
444
|
-
headers: {
|
|
445
|
-
'User-Agent': USER_AGENT,
|
|
446
|
-
},
|
|
447
|
-
})
|
|
448
|
-
.then(async (res) => {
|
|
449
|
-
const contentType = res.headers.get('content-type');
|
|
450
|
-
const text = await res.text();
|
|
451
|
-
|
|
452
|
-
return extractBodyContent(text, contentType, url);
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function isURL(url) {
|
|
457
|
-
try {
|
|
458
|
-
return !!new URL(url);
|
|
459
|
-
} catch (e) {
|
|
460
|
-
return false;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function extractBodyContent(text, contentType, url) {
|
|
465
|
-
const parsed = tryParse(text);
|
|
466
|
-
|
|
467
|
-
// Try JSON
|
|
468
|
-
if (parsed) {
|
|
469
|
-
// If it's from rss.app, extract the content
|
|
470
|
-
if (parsed.items) {
|
|
471
|
-
return parsed.items.map((i) => `${i.title}: ${i.content_text}`).join('\n');
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// If we can't recognize the JSON, return the original text
|
|
475
|
-
return text;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// Extract the content within the body tag
|
|
479
|
-
const bodyMatch = text.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
480
|
-
if (!bodyMatch) {
|
|
481
|
-
return '';
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
let bodyContent = bodyMatch[1];
|
|
485
|
-
|
|
486
|
-
// Remove script and meta tags
|
|
487
|
-
bodyContent = bodyContent.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
488
|
-
bodyContent = bodyContent.replace(/<meta[^>]*>/gi, '');
|
|
489
|
-
|
|
490
|
-
// Remove remaining HTML tags
|
|
491
|
-
return bodyContent.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function tryParse(json) {
|
|
495
|
-
try {
|
|
496
|
-
return JSON5.parse(json);
|
|
497
|
-
} catch (e) {
|
|
498
|
-
return null;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function randomize(array) {
|
|
503
|
-
return array.sort(() => Math.random() - 0.5);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
module.exports.resolveSource = resolveSource;
|
|
507
|
-
module.exports.processFeedSource = processFeedSource;
|
|
508
|
-
module.exports.feedItemHash = feedItemHash;
|
|
509
|
-
module.exports.getProcessedItemIds = getProcessedItemIds;
|
|
510
|
-
module.exports.trackFeedItem = trackFeedItem;
|
|
511
|
-
module.exports.isURL = isURL;
|