godcrm/backend/services/content-aggregator/crm-storage.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

222 lines
6.6 KiB
JavaScript

// backend/services/content-aggregator/crm-storage.js
// CRM storage and deduplication for Content Aggregation Service
import { CRM_API_BASE, CONTENT_TABLE_ID } from './config.js';
import { log, truncate, fetchJSON, getCRMAuthToken } from './helpers.js';
/**
* Fetch existing content titles from CRM table to deduplicate
* @param {string|null} token - JWT auth token
* @returns {Promise<Set<string>>} Set of existing titles (lowercased)
*/
export async function getExistingTitles(token) {
try {
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const resp = await fetchJSON(
`${CRM_API_BASE}/tables/${CONTENT_TABLE_ID}/rows?limit=500`,
{ headers },
20000
);
const rows = resp?.data?.rows || resp?.data || [];
const titles = new Set();
for (const row of rows) {
const data = row.data || row;
const title = (data.title || '').toLowerCase().trim();
if (title) titles.add(title);
}
log.info({ existingCount: titles.size }, 'CRM: loaded existing titles for dedup');
return titles;
} catch (err) {
log.warn({ err: err.message }, 'CRM: failed to load existing titles (will skip dedup)');
return new Set();
}
}
/**
* Store a content item in CRM table 2603
* @param {object} item - Content item to store
* @param {string|null} token - JWT auth token
* @returns {Promise<{success: boolean, id?: number, error?: string}>}
*/
export async function storeInCRM(item, token) {
try {
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
// Build the body field: combine description + metadata
const bodyParts = [];
if (item.description || item.selftext) {
bodyParts.push(item.description || item.selftext);
}
if (item.url) {
bodyParts.push(`\nLink: ${item.url}`);
}
if (item.score) {
bodyParts.push(`Score: ${item.score}`);
}
if (item.author) {
bodyParts.push(`Author: ${item.author}`);
}
if (item.subreddit) {
bodyParts.push(`Subreddit: r/${item.subreddit}`);
}
if (item.source_name) {
bodyParts.push(`Feed: ${item.source_name}`);
}
const rowData = {
title: truncate(item.title, 255),
body: bodyParts.join('\n'),
status: 'draft',
platform: 'telegram',
source: item.source || 'unknown',
source_url: item.url || '',
source_id: item.source_id || '',
score: item.score || 0,
fetched_at: new Date().toISOString(),
};
const resp = await fetchJSON(
`${CRM_API_BASE}/tables/${CONTENT_TABLE_ID}/rows`,
{
method: 'POST',
headers,
body: JSON.stringify({ data: rowData }),
},
15000
);
const newId = resp?.data?.id || resp?.id;
return { success: true, id: newId };
} catch (err) {
log.warn({ err: err.message, title: item.title }, 'CRM: failed to store item');
return { success: false, error: err.message };
}
}
/**
* Fetch queued (draft) content from CRM table
* @returns {Promise<Array>}
*/
export async function getContentQueue() {
try {
const token = await getCRMAuthToken();
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const resp = await fetchJSON(
`${CRM_API_BASE}/tables/${CONTENT_TABLE_ID}/rows?limit=100`,
{ headers },
20000
);
const rows = resp?.data?.rows || resp?.data || [];
// Filter to draft/pending items
return rows.filter(row => {
const data = row.data || row;
const status = (data.status || '').toLowerCase();
return status === 'draft' || status === 'pending' || status === '';
}).map(row => {
const data = row.data || row;
return {
id: row.id,
title: data.title,
body: data.body,
source: data.source,
source_url: data.source_url,
status: data.status,
platform: data.platform,
score: data.score,
fetched_at: data.fetched_at,
created_at: row.created_at,
};
});
} catch (err) {
log.error({ err: err.message }, 'Failed to fetch content queue');
throw err;
}
}
/**
* Publish a content item to Telegram channel
* @param {number} rowId - CRM row ID
* @returns {Promise<{success: boolean, message?: string, error?: string}>}
*/
export async function publishToTelegram(rowId) {
try {
const token = await getCRMAuthToken();
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
// Fetch the row
const resp = await fetchJSON(
`${CRM_API_BASE}/tables/${CONTENT_TABLE_ID}/rows/${rowId}`,
{ headers },
15000
);
const row = resp?.data;
if (!row) {
return { success: false, error: 'Row not found' };
}
const data = row.data || row;
const title = data.title || 'Untitled';
const body = data.body || '';
const sourceUrl = data.source_url || '';
// Format message for Telegram
const lines = [];
lines.push(`*${title.replace(/([_*`\[\]])/g, '\\$1')}*`);
if (body) {
lines.push('');
// Take first 300 chars of body for Telegram
const snippet = body.length > 300 ? body.slice(0, 300) + '...' : body;
lines.push(snippet.replace(/([_*`\[\]])/g, '\\$1'));
}
if (sourceUrl) {
lines.push('');
lines.push(`[Read more](${sourceUrl})`);
}
lines.push('');
lines.push(`Source: ${data.source || 'web'}`);
const message = lines.join('\n');
// Send via TelegramService — post to public channel, not admin chat
const { sendChannelPost } = await import('../TelegramService.js');
const tgResult = await sendChannelPost(message);
if (tgResult.success) {
// Update row status to 'published'
try {
await fetchJSON(
`${CRM_API_BASE}/tables/${CONTENT_TABLE_ID}/rows/${rowId}`,
{
method: 'PUT',
headers,
body: JSON.stringify({
data: { ...data, status: 'published', published_at: new Date().toISOString() },
}),
},
15000
);
} catch (updateErr) {
log.warn({ err: updateErr.message, rowId }, 'Failed to update row status after publish');
}
return { success: true, message: 'Published to Telegram', messageId: tgResult.messageId };
} else {
return { success: false, error: tgResult.error || 'Telegram send failed' };
}
} catch (err) {
log.error({ err: err.message, rowId }, 'Failed to publish to Telegram');
return { success: false, error: err.message };
}
}