backend-manager 5.9.16 → 5.9.18
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/docs/ghostii.md +63 -5
- package/package.json +1 -1
- package/src/manager/events/cron/daily/blog-auto-publisher.js +23 -398
- package/src/manager/libraries/content/ghostii.js +2 -1
- package/src/manager/libraries/content/source-resolver.js +397 -0
- package/src/manager/libraries/email/generators/newsletter.js +18 -46
- package/src/manager/routes/admin/post/post.js +1 -0
- package/src/manager/routes/admin/post/templates/post.html +1 -0
- package/src/manager/routes/content/post/get.js +1 -0
- package/src/test/fixtures/firebase-project/.temp/test-mode.json +1 -1
- package/src/test/fixtures/firebase-project/database-debug.log +8 -8
- package/src/test/fixtures/firebase-project/firestore-debug.log +56 -56
- package/src/test/fixtures/firebase-project/pubsub-debug.log +3 -3
- package/test/email/newsletter-generate.js +65 -50
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared content-source resolution for blog + newsletter pipelines.
|
|
3
|
+
*
|
|
4
|
+
* Both blog-auto-publisher and newsletter-generator need to:
|
|
5
|
+
* - Resolve $feed:, $parent, $brand, URL, and text sources
|
|
6
|
+
* - Track used sources in Firestore (content-sources collection)
|
|
7
|
+
* - Deduplicate across runs
|
|
8
|
+
* - Follow anti-traceability rules
|
|
9
|
+
*
|
|
10
|
+
* This module is the single source of truth for all of that.
|
|
11
|
+
*/
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const fetch = require('wonderful-fetch');
|
|
14
|
+
|
|
15
|
+
const { parseFeed, extractArticleContent } = require('./feed-parser.js');
|
|
16
|
+
|
|
17
|
+
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';
|
|
18
|
+
const FEED_PREFIX = '$feed:';
|
|
19
|
+
const CONTENT_SOURCES_COLLECTION = 'content-sources';
|
|
20
|
+
|
|
21
|
+
// ── Prompt constants ─────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const ANTI_TRACEABILITY = [
|
|
24
|
+
'ATTRIBUTION RULES:',
|
|
25
|
+
'- NEVER name the source publication, newsletter, blog, or author.',
|
|
26
|
+
'- NEVER use phrases like "according to sources", "a recent article said", "as reported by", or similar.',
|
|
27
|
+
'- Write AS IF the source did not exist — content should read as original, first-party work.',
|
|
28
|
+
'- If the source mentions a third-party platform, product, or company by name, THAT is fine — those are subjects, not sources.',
|
|
29
|
+
'- Paraphrase all facts, figures, and data. Never copy exact numbers verbatim.',
|
|
30
|
+
].join('\n');
|
|
31
|
+
|
|
32
|
+
const PROMPT_SOURCE = `
|
|
33
|
+
Company: {brand.brand.name}: {brand.brand.description}
|
|
34
|
+
Date: {date}
|
|
35
|
+
Instructions: {instructions}
|
|
36
|
+
Tone: {tone}
|
|
37
|
+
Categories: {categories}
|
|
38
|
+
Keywords: {keywords}
|
|
39
|
+
|
|
40
|
+
Write an article covering the SAME topic as this source material.
|
|
41
|
+
Keep the specific subject matter — names, companies, events, dates, numbers.
|
|
42
|
+
Do NOT copy the source text — rewrite it in your own voice with a different angle, title, and structure.
|
|
43
|
+
The article should read as if written by a completely different author covering the same subject.
|
|
44
|
+
${ANTI_TRACEABILITY}
|
|
45
|
+
Source title: {sourceTitle}
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
const PROMPT = `
|
|
49
|
+
Company: {brand.brand.name}: {brand.brand.description}
|
|
50
|
+
Date: {date}
|
|
51
|
+
Instructions: {instructions}
|
|
52
|
+
Tone: {tone}
|
|
53
|
+
Categories: {categories}
|
|
54
|
+
Keywords: {keywords}
|
|
55
|
+
|
|
56
|
+
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):
|
|
57
|
+
{suggestion}
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
// ── Feed source processing ───────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
async function processFeedSource(assistant, feedUrl, brandId, admin) {
|
|
63
|
+
const feedText = await fetch(feedUrl, {
|
|
64
|
+
timeout: 30000,
|
|
65
|
+
tries: 2,
|
|
66
|
+
response: 'text',
|
|
67
|
+
headers: { 'User-Agent': USER_AGENT },
|
|
68
|
+
}).catch((e) => e);
|
|
69
|
+
|
|
70
|
+
if (feedText instanceof Error) {
|
|
71
|
+
assistant.error(`processFeedSource(): Failed to fetch feed: ${feedUrl}`, feedText);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { items } = parseFeed(feedText);
|
|
76
|
+
if (!items.length) {
|
|
77
|
+
assistant.log(`processFeedSource(): No items in feed: ${feedUrl}`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
assistant.log(`processFeedSource(): Parsed ${items.length} items from feed`);
|
|
82
|
+
|
|
83
|
+
const processedIds = await getProcessedItemIds(admin, feedUrl).catch((e) => {
|
|
84
|
+
assistant.error('processFeedSource(): Error querying tracked items (continuing)', e);
|
|
85
|
+
return new Set();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const unprocessed = items.filter((item) => !processedIds.has(item.id) && !processedIds.has(item.url));
|
|
89
|
+
if (!unprocessed.length) {
|
|
90
|
+
assistant.log(`processFeedSource(): All ${items.length} items already processed for: ${feedUrl}`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
assistant.log(`processFeedSource(): ${unprocessed.length} unprocessed items available`);
|
|
95
|
+
|
|
96
|
+
const item = unprocessed[0];
|
|
97
|
+
|
|
98
|
+
let content = '';
|
|
99
|
+
if (item.url) {
|
|
100
|
+
content = await extractArticleContent(item.url).catch((e) => {
|
|
101
|
+
assistant.error(`processFeedSource(): Failed to extract content from ${item.url} (using summary)`, e);
|
|
102
|
+
return '';
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!content || content.length < 100) {
|
|
107
|
+
content = item.content || item.summary || '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { item, content };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Parent source processing ─────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
async function processParentSource(assistant, entry, admin, Manager) {
|
|
116
|
+
const parentUrl = Manager?.getParentApiUrl?.();
|
|
117
|
+
|
|
118
|
+
if (!parentUrl) {
|
|
119
|
+
assistant.log('processParentSource(): No parent URL configured');
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const categories = entry.categories || [];
|
|
124
|
+
const allSources = [];
|
|
125
|
+
const categoriesToFetch = categories.length ? categories : [''];
|
|
126
|
+
|
|
127
|
+
for (const category of categoriesToFetch) {
|
|
128
|
+
const query = {
|
|
129
|
+
limit: 3,
|
|
130
|
+
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (category) {
|
|
134
|
+
query.category = category;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const data = await fetch(`${parentUrl}/newsletter-sources`, {
|
|
138
|
+
method: 'get',
|
|
139
|
+
response: 'json',
|
|
140
|
+
timeout: 60000,
|
|
141
|
+
query: query,
|
|
142
|
+
}).catch((e) => {
|
|
143
|
+
assistant.error(`processParentSource(): Failed to fetch sources for category=${category}: ${e.message}`);
|
|
144
|
+
return null;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (data?.sources?.length) {
|
|
148
|
+
allSources.push(...data.sources);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!allSources.length) {
|
|
153
|
+
assistant.log('processParentSource(): No sources available from parent');
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (admin) {
|
|
158
|
+
const usedUrls = new Set();
|
|
159
|
+
const snapshot = await admin.firestore()
|
|
160
|
+
.collection(CONTENT_SOURCES_COLLECTION)
|
|
161
|
+
.where('origin', '==', '$parent')
|
|
162
|
+
.select('url')
|
|
163
|
+
.get()
|
|
164
|
+
.catch((e) => {
|
|
165
|
+
assistant.error('processParentSource(): Error querying tracked sources (continuing)', e);
|
|
166
|
+
return { docs: [] };
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
snapshot.docs.forEach((doc) => {
|
|
170
|
+
const data = doc.data();
|
|
171
|
+
if (data.url) {
|
|
172
|
+
usedUrls.add(data.url);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const available = allSources.filter((s) => !usedUrls.has(s.url || s.id));
|
|
177
|
+
|
|
178
|
+
if (!available.length) {
|
|
179
|
+
assistant.log('processParentSource(): All parent sources already used');
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return available;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return allSources;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Parent source fetching for newsletter ────────────────────────────
|
|
190
|
+
|
|
191
|
+
async function fetchParentSources(parentUrl, categories, assistant) {
|
|
192
|
+
const allSources = [];
|
|
193
|
+
|
|
194
|
+
for (const category of categories) {
|
|
195
|
+
try {
|
|
196
|
+
const data = await fetch(`${parentUrl}/newsletter-sources`, {
|
|
197
|
+
method: 'get',
|
|
198
|
+
response: 'json',
|
|
199
|
+
timeout: 60000,
|
|
200
|
+
query: {
|
|
201
|
+
category,
|
|
202
|
+
limit: 3,
|
|
203
|
+
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (data.sources?.length) {
|
|
208
|
+
allSources.push(...data.sources);
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
assistant.error(`fetchParentSources(): Failed to fetch ${category} sources: ${e.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return allSources;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Feed-to-newsletter normalization ─────────────────────────────────
|
|
219
|
+
|
|
220
|
+
function normalizeFeedItemForNewsletter(feedResult, feedUrl, categories) {
|
|
221
|
+
const item = feedResult.item;
|
|
222
|
+
let hostname = '';
|
|
223
|
+
try { hostname = new URL(feedUrl).hostname; } catch (e) { /* ignore */ }
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
id: item.id || item.url || '',
|
|
227
|
+
title: item.title || '',
|
|
228
|
+
subject: item.title || '',
|
|
229
|
+
category: (categories && categories[0]) || '',
|
|
230
|
+
categories: categories || [],
|
|
231
|
+
url: item.url || '',
|
|
232
|
+
source: {
|
|
233
|
+
from: hostname,
|
|
234
|
+
subject: item.title || '',
|
|
235
|
+
content: feedResult.content || item.content || item.summary || '',
|
|
236
|
+
},
|
|
237
|
+
ai: null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── Resolve newsletter sources (feeds + parent + fallback) ───────────
|
|
242
|
+
|
|
243
|
+
async function resolveNewsletterSources({ sources, categories, admin, Manager, assistant }) {
|
|
244
|
+
const resolved = [];
|
|
245
|
+
|
|
246
|
+
const feedUrls = (sources || []).filter((s) => typeof s === 'string' && s.startsWith(FEED_PREFIX));
|
|
247
|
+
const hasParent = (sources || []).includes('$parent');
|
|
248
|
+
|
|
249
|
+
const brandId = Manager?.config?.brand?.id || '';
|
|
250
|
+
|
|
251
|
+
for (const source of feedUrls) {
|
|
252
|
+
const feedUrl = source.slice(FEED_PREFIX.length);
|
|
253
|
+
const feedResult = await processFeedSource(assistant, feedUrl, brandId, admin);
|
|
254
|
+
if (feedResult) {
|
|
255
|
+
resolved.push(normalizeFeedItemForNewsletter(feedResult, feedUrl, categories));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (hasParent) {
|
|
260
|
+
const parentUrl = Manager?.getParentApiUrl?.();
|
|
261
|
+
if (parentUrl) {
|
|
262
|
+
const parentSources = await fetchParentSources(parentUrl, categories || [], assistant);
|
|
263
|
+
resolved.push(...parentSources);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return resolved;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Firestore tracking ──────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
async function trackContentSource(admin, { url, origin, feedUrl, itemId, itemTitle, usedBy, brandId, postUrl, postSlug }) {
|
|
273
|
+
const docId = contentSourceHash(origin || '', url || '');
|
|
274
|
+
const nowISO = new Date().toISOString();
|
|
275
|
+
const nowUNIX = Math.round(Date.now() / 1000);
|
|
276
|
+
|
|
277
|
+
await admin.firestore().doc(`${CONTENT_SOURCES_COLLECTION}/${docId}`).set({
|
|
278
|
+
url: url || null,
|
|
279
|
+
origin: origin || null,
|
|
280
|
+
feedUrl: feedUrl || null,
|
|
281
|
+
itemId: itemId || null,
|
|
282
|
+
itemTitle: itemTitle || null,
|
|
283
|
+
usedBy: usedBy || null,
|
|
284
|
+
brandId: brandId || null,
|
|
285
|
+
postUrl: postUrl || null,
|
|
286
|
+
postSlug: postSlug || null,
|
|
287
|
+
metadata: {
|
|
288
|
+
created: { timestamp: nowISO, timestampUNIX: nowUNIX },
|
|
289
|
+
updated: { timestamp: nowISO, timestampUNIX: nowUNIX },
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function contentSourceHash(origin, url) {
|
|
295
|
+
return crypto.createHash('sha256').update(`${origin}::${url}`).digest('hex').slice(0, 20);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function getProcessedItemIds(admin, feedUrl) {
|
|
299
|
+
if (!admin) {
|
|
300
|
+
return new Set();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const snapshot = await admin.firestore()
|
|
304
|
+
.collection(CONTENT_SOURCES_COLLECTION)
|
|
305
|
+
.where('feedUrl', '==', feedUrl)
|
|
306
|
+
.select('itemId', 'url')
|
|
307
|
+
.get();
|
|
308
|
+
|
|
309
|
+
const ids = new Set();
|
|
310
|
+
snapshot.docs.forEach((doc) => {
|
|
311
|
+
const data = doc.data();
|
|
312
|
+
if (data.itemId) {
|
|
313
|
+
ids.add(data.itemId);
|
|
314
|
+
}
|
|
315
|
+
if (data.url) {
|
|
316
|
+
ids.add(data.url);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return ids;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── URL content extraction ──────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
function getURLContent(url) {
|
|
326
|
+
return fetch(url, {
|
|
327
|
+
timeout: 120000,
|
|
328
|
+
tries: 3,
|
|
329
|
+
response: 'raw',
|
|
330
|
+
headers: { 'User-Agent': USER_AGENT },
|
|
331
|
+
})
|
|
332
|
+
.then(async (res) => {
|
|
333
|
+
const contentType = res.headers.get('content-type');
|
|
334
|
+
const text = await res.text();
|
|
335
|
+
return extractBodyContent(text, contentType, url);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isURL(url) {
|
|
340
|
+
try {
|
|
341
|
+
return !!new URL(url);
|
|
342
|
+
} catch (e) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function extractBodyContent(text) {
|
|
348
|
+
const parsed = tryParse(text);
|
|
349
|
+
|
|
350
|
+
if (parsed) {
|
|
351
|
+
if (parsed.items) {
|
|
352
|
+
return parsed.items.map((i) => `${i.title}: ${i.content_text}`).join('\n');
|
|
353
|
+
}
|
|
354
|
+
return text;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const bodyMatch = text.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
358
|
+
if (!bodyMatch) {
|
|
359
|
+
return '';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let bodyContent = bodyMatch[1];
|
|
363
|
+
bodyContent = bodyContent.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
364
|
+
bodyContent = bodyContent.replace(/<meta[^>]*>/gi, '');
|
|
365
|
+
|
|
366
|
+
return bodyContent.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function tryParse(json) {
|
|
370
|
+
try {
|
|
371
|
+
return require('json5').parse(json);
|
|
372
|
+
} catch (e) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Exports ─────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
module.exports = {
|
|
380
|
+
PROMPT_SOURCE,
|
|
381
|
+
PROMPT,
|
|
382
|
+
ANTI_TRACEABILITY,
|
|
383
|
+
FEED_PREFIX,
|
|
384
|
+
CONTENT_SOURCES_COLLECTION,
|
|
385
|
+
USER_AGENT,
|
|
386
|
+
processFeedSource,
|
|
387
|
+
processParentSource,
|
|
388
|
+
fetchParentSources,
|
|
389
|
+
resolveNewsletterSources,
|
|
390
|
+
normalizeFeedItemForNewsletter,
|
|
391
|
+
trackContentSource,
|
|
392
|
+
contentSourceHash,
|
|
393
|
+
getProcessedItemIds,
|
|
394
|
+
getURLContent,
|
|
395
|
+
isURL,
|
|
396
|
+
extractBodyContent,
|
|
397
|
+
};
|
|
@@ -42,7 +42,7 @@ const { renderMarkdown } = require('./lib/markdown-renderer.js');
|
|
|
42
42
|
const { uploadAssets, RAW_BASE } = require('./lib/image-host.js');
|
|
43
43
|
const { buildPublicConfig } = require('../../../routes/brand/get.js');
|
|
44
44
|
const { writeArticle, publishArticle } = require('../../../libraries/content/ghostii.js');
|
|
45
|
-
const { trackContentSource, contentSourceHash } = require('../../../
|
|
45
|
+
const { trackContentSource, contentSourceHash, resolveNewsletterSources } = require('../../../libraries/content/source-resolver.js');
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Generate newsletter content from parent server sources.
|
|
@@ -99,24 +99,22 @@ async function generate(Manager, assistant, settings, opts = {}) {
|
|
|
99
99
|
let sources = opts.sources;
|
|
100
100
|
|
|
101
101
|
if (!sources) {
|
|
102
|
-
|
|
103
|
-
if (contentSources.includes('$parent')) {
|
|
104
|
-
const categories = config.categories || [];
|
|
102
|
+
const categories = config.categories || [];
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const parentUrl = Manager.getParentApiUrl();
|
|
104
|
+
if (!categories.length) {
|
|
105
|
+
assistant.log('Newsletter generator: no categories configured');
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
112
108
|
|
|
113
|
-
|
|
114
|
-
assistant.log('Newsletter generator: no parent URL configured');
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
109
|
+
const admin = Manager.libraries?.admin;
|
|
117
110
|
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
sources = await resolveNewsletterSources({
|
|
112
|
+
sources: contentSources,
|
|
113
|
+
categories,
|
|
114
|
+
admin,
|
|
115
|
+
Manager,
|
|
116
|
+
assistant,
|
|
117
|
+
});
|
|
120
118
|
}
|
|
121
119
|
|
|
122
120
|
if (!sources?.length) {
|
|
@@ -402,9 +400,11 @@ async function generate(Manager, assistant, settings, opts = {}) {
|
|
|
402
400
|
const admin = Manager.libraries?.admin;
|
|
403
401
|
if (admin) {
|
|
404
402
|
for (const source of sources) {
|
|
403
|
+
const origin = source.source?.from?.startsWith?.('http') ? `$feed:${source.source.from}` : '$parent';
|
|
405
404
|
await trackContentSource(admin, {
|
|
406
405
|
url: source.url || source.id,
|
|
407
|
-
origin
|
|
406
|
+
origin,
|
|
407
|
+
feedUrl: origin.startsWith('$feed:') ? origin.slice(6) : undefined,
|
|
408
408
|
itemId: source.id,
|
|
409
409
|
itemTitle: source.title || '',
|
|
410
410
|
usedBy: 'newsletter',
|
|
@@ -696,35 +696,7 @@ function aggregateTotals(filterMeta, structureMeta, images) {
|
|
|
696
696
|
};
|
|
697
697
|
}
|
|
698
698
|
|
|
699
|
-
|
|
700
|
-
* Fetch ready newsletter sources from the parent server.
|
|
701
|
-
*/
|
|
702
|
-
async function fetchSources(parentUrl, categories, assistant) {
|
|
703
|
-
const allSources = [];
|
|
704
|
-
|
|
705
|
-
for (const category of categories) {
|
|
706
|
-
try {
|
|
707
|
-
const data = await fetch(`${parentUrl}/newsletter-sources`, {
|
|
708
|
-
method: 'get',
|
|
709
|
-
response: 'json',
|
|
710
|
-
timeout: 60000,
|
|
711
|
-
query: {
|
|
712
|
-
category,
|
|
713
|
-
limit: 3,
|
|
714
|
-
backendManagerKey: process.env.BACKEND_MANAGER_KEY,
|
|
715
|
-
},
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
if (data.sources?.length) {
|
|
719
|
-
allSources.push(...data.sources);
|
|
720
|
-
}
|
|
721
|
-
} catch (e) {
|
|
722
|
-
assistant.error(`Newsletter generator: Failed to fetch ${category} sources: ${e.message}`);
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
return allSources;
|
|
727
|
-
}
|
|
699
|
+
// fetchSources() removed — replaced by resolveNewsletterSources() from source-resolver.js
|
|
728
700
|
|
|
729
701
|
|
|
730
702
|
/**
|
|
@@ -87,6 +87,7 @@ module.exports = async ({ assistant, Manager, user, settings, analytics }) => {
|
|
|
87
87
|
settings.tags = settings.tags;
|
|
88
88
|
settings.categories = settings.categories;
|
|
89
89
|
settings.layout = settings.layout;
|
|
90
|
+
settings.source = settings.source || null;
|
|
90
91
|
settings.date = moment(settings.date || now).subtract(1, 'days').format('YYYY-MM-DD');
|
|
91
92
|
settings.id = settings.id || Math.round(new Date(now).getTime() / 1000);
|
|
92
93
|
settings.directory = `src/_posts/${moment(now).format('YYYY')}/${settings.postPath}`;
|
|
@@ -100,6 +100,7 @@ module.exports = async ({ assistant, Manager, settings, analytics }) => {
|
|
|
100
100
|
id: parsed.post.id,
|
|
101
101
|
tags: parsed.post.tags,
|
|
102
102
|
categories: parsed.post.categories,
|
|
103
|
+
source: parsed.post.source || null,
|
|
103
104
|
|
|
104
105
|
// Derived
|
|
105
106
|
headerImageURL: `${url.origin}/assets/images/blog/post-${parsed.post.id}/${filename}.jpg`,
|
|
@@ -2,11 +2,11 @@ WARNING: A terminally deprecated method in sun.misc.Unsafe has been called
|
|
|
2
2
|
WARNING: sun.misc.Unsafe::objectFieldOffset has been called by akka.util.Unsafe (file:/Users/ian/.cache/firebase/emulators/firebase-database-emulator-v4.11.2.jar)
|
|
3
3
|
WARNING: Please consider reporting this to the maintainers of class akka.util.Unsafe
|
|
4
4
|
WARNING: sun.misc.Unsafe::objectFieldOffset will be removed in a future release
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
17:05:48.613 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
|
|
6
|
+
17:05:48.710 [main] INFO com.firebase.server.forge.App$ - Listening at 127.0.0.1:9000
|
|
7
|
+
17:05:58.279 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.NamespaceActor - demo-backend-manager-default-rtdb successfully activated FBKV (SurveyIdle(0)) wait: 70ms, init: 0ms
|
|
8
|
+
17:05:58.314 [NamespaceSystem-blocking-namespace-operation-dispatcher-6] INFO com.firebase.core.namespace.StateManager - Namespace demo-backend-manager-default-rtdb status Active to Active
|
|
9
|
+
17:06:10.472 [Thread-0] INFO com.firebase.server.forge.App$ - Attempting graceful shutdown.
|
|
10
|
+
17:06:10.477 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.Terminator$Terminator - 1 actors left to terminate: demo-backend-manager-default-rtdb
|
|
11
|
+
17:06:10.480 [NamespaceSystem-akka.actor.default-dispatcher-4] INFO com.firebase.core.namespace.NamespaceActor - stopped namespace actor for demo-backend-manager-default-rtdb
|
|
12
|
+
17:06:10.483 [Thread-0] INFO com.firebase.server.forge.App$ - Graceful shutdown complete.
|