| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- const pull = require('../server/node_modules/pull-stream');
- const util = require('../server/node_modules/util');
- const axios = require('../server/node_modules/axios');
- const fs = require('fs');
- const path = require('path');
- const { getConfig } = require('../configs/config-manager.js');
- const logLimit = getConfig().ssbLogStream?.limit || 1000;
- const CYCLE_PATH = path.join(__dirname, '..', 'configs', 'blockchain-cycle.json');
- const readCycle = () => {
- try { return JSON.parse(fs.readFileSync(CYCLE_PATH, 'utf8')).cycle || 0; }
- catch { return 0; }
- };
- const DAY_MS = 24 * 60 * 60 * 1000;
- const WEEK_MS = 7 * DAY_MS;
- const MONTH_MS = 30 * DAY_MS;
- const YEAR_MS = 365 * DAY_MS;
- const FILTER_WINDOWS = {
- today: DAY_MS,
- week: WEEK_MS,
- month: MONTH_MS,
- year: YEAR_MS,
- always: null
- };
- const ACTION_TYPES = new Set([
- 'post', 'about', 'contact', 'feed', 'bookmark', 'image', 'audio', 'video',
- 'document', 'torrent', 'event', 'task', 'taskAssignment',
- 'votes', 'vote', 'report', 'tribe', 'chat', 'chatMessage', 'pad', 'padEntry',
- 'forum', 'market', 'job', 'project', 'pixelia', 'map', 'mapMarker',
- 'shop', 'shopProduct', 'curriculum', 'gameScore',
- 'calendar', 'calendarDate', 'calendarNote',
- 'transfer', 'bankClaim', 'ubiClaim',
- 'parliamentCandidature', 'parliamentProposal', 'parliamentLaw',
- 'parliamentTerm', 'parliamentRevocation',
- 'courtsCase', 'courtsEvidence', 'courtsAnswer', 'courtsVerdict',
- 'courtsNomination', 'courtsNominationVote',
- 'courtsSettlementProposal', 'courtsSettlementAccepted',
- 'tribeParliamentCandidature', 'tribeParliamentRule'
- ]);
- const ACTION_PHRASES = {
- post: 'published a post',
- about: 'updated profile information',
- contact: 'followed or unfollowed someone',
- feed: 'shared content in the feed',
- bookmark: 'bookmarked a resource',
- image: 'uploaded an image',
- audio: 'uploaded an audio track',
- video: 'uploaded a video',
- document: 'uploaded a document',
- torrent: 'shared a torrent',
- event: 'created an event',
- task: 'created a task',
- taskAssignment: 'updated a task assignment',
- votes: 'participated in a vote',
- vote: 'cast a vote',
- report: 'submitted a report',
- tribe: 'interacted with a tribe',
- chat: 'opened a chat room',
- chatMessage: 'sent a chat message',
- pad: 'worked on a collaborative pad',
- padEntry: 'edited a pad entry',
- market: 'posted in the market',
- forum: 'posted in the forum',
- job: 'posted a job opportunity',
- project: 'advanced a project',
- pixelia: 'placed a pixel in pixelia',
- map: 'contributed to a map',
- mapMarker: 'placed a marker on a map',
- shop: 'updated a shop',
- shopProduct: 'managed a shop product',
- curriculum: 'edited the curriculum',
- gameScore: 'logged a game score',
- calendar: 'managed a calendar',
- calendarDate: 'added a calendar date',
- calendarNote: 'added a calendar note',
- transfer: 'sent or confirmed a transfer',
- bankClaim: 'completed a banking claim',
- ubiClaim: 'claimed the UBI',
- parliamentCandidature: 'published a parliamentary candidature',
- parliamentProposal: 'published a parliamentary proposal',
- parliamentLaw: 'participated in a parliamentary law',
- parliamentTerm: 'participated in a parliamentary term',
- parliamentRevocation: 'submitted a parliamentary revocation',
- courtsCase: 'opened a courts case',
- courtsEvidence: 'submitted courts evidence',
- courtsAnswer: 'replied in a courts case',
- courtsVerdict: 'reached a courts verdict',
- courtsNomination: 'nominated a judge',
- courtsNominationVote: 'voted on a judge nomination',
- courtsSettlementProposal: 'proposed a courts settlement',
- courtsSettlementAccepted: 'accepted a courts settlement',
- tribeParliamentCandidature: 'stood for a tribe parliament',
- tribeParliamentRule: 'contributed a tribe parliament rule'
- };
- const compact = (s, n = 200) => String(s || '').replace(/\s+/g, ' ').trim().slice(0, n);
- module.exports = ({ cooler }) => {
- let ssb;
- let userId;
- const openSsb = async () => {
- if (!ssb) {
- ssb = await cooler.open();
- userId = ssb.id;
- }
- return ssb;
- };
- async function listAllUserActions() {
- const ssbClient = await openSsb();
- const msgs = await new Promise((resolve, reject) =>
- pull(
- ssbClient.createLogStream({ reverse: true, limit: logLimit }),
- pull.collect((err, arr) => err ? reject(err) : resolve(arr))
- )
- );
- const out = [];
- for (const m of msgs) {
- const v = m?.value || {};
- const c = v?.content;
- if (!c || typeof c !== 'object' || !c.type) continue;
- if (v.author !== userId) continue;
- if (c.type === 'log') continue;
- if (!ACTION_TYPES.has(c.type)) continue;
- const ts = v.timestamp || 0;
- const summary = c.title || c.text || c.question || c.subject || c.name || c.concept || c.description || '';
- out.push({ key: m.key, ts, type: c.type, summary: compact(summary) });
- }
- return out;
- }
- async function callAI(prompt) {
- if (!prompt) return '';
- const tryOnce = async () => {
- try {
- const res = await axios.post('http://localhost:4001/ai', { input: prompt, raw: true }, { timeout: 90000 });
- return String(res?.data?.answer || '').trim();
- } catch { return ''; }
- };
- let out = await tryOnce();
- if (!out) {
- await new Promise(r => setTimeout(r, 2000));
- out = await tryOnce();
- }
- return out;
- }
- function buildActionPrompt(a) {
- const d = new Date(a.ts).toISOString().slice(0, 16).replace('T', ' ');
- const ctx = a.summary ? ` Subject: "${compact(a.summary, 120)}".` : '';
- return `One first-person diary sentence about a "${a.type}" action at ${d}.${ctx} Vary phrasing. No IDs, hashes, quotes, lists or markdown.`;
- }
- function buildFallbackSentence(a) {
- const phrase = ACTION_PHRASES[a.type] || `performed a ${a.type} action`;
- const d = new Date(a.ts).toISOString().slice(0, 16).replace('T', ' ');
- const ctx = a.summary ? ` — ${compact(a.summary, 120)}` : '';
- return `At ${d} I ${phrase}${ctx}.`;
- }
- function isAImodOn() {
- try { return getConfig().modules?.aiMod === 'on'; } catch { return false; }
- }
- async function publishLog({ text, label, mode, ref }) {
- const ssbClient = await openSsb();
- const content = {
- type: 'log',
- text: String(text || '').slice(0, 8000),
- label: String(label || '').slice(0, 200),
- mode: mode === 'ai' ? 'ai' : 'manual',
- cycle: readCycle(),
- createdAt: new Date().toISOString(),
- timestamp: Date.now(),
- private: true
- };
- if (ref) content.ref = String(ref);
- const publishAsync = util.promisify(ssbClient.private.publish);
- return publishAsync(content, [userId]);
- }
- async function republishLog({ replaces, text, label, mode, cycle, createdAt }) {
- const ssbClient = await openSsb();
- const content = {
- type: 'log',
- replaces,
- text: String(text || '').slice(0, 8000),
- label: String(label || '').slice(0, 200),
- mode: mode === 'ai' ? 'ai' : 'manual',
- cycle: cycle || readCycle(),
- createdAt: createdAt || new Date().toISOString(),
- updatedAt: new Date().toISOString(),
- timestamp: Date.now(),
- private: true
- };
- const publishAsync = util.promisify(ssbClient.private.publish);
- return publishAsync(content, [userId]);
- }
- async function publishTombstone(target) {
- const ssbClient = await openSsb();
- const content = {
- type: 'tombstone',
- target,
- deletedAt: new Date().toISOString(),
- author: userId,
- private: true
- };
- const publishAsync = util.promisify(ssbClient.private.publish);
- return publishAsync(content, [userId]);
- }
- async function createManual(label, text) {
- await openSsb();
- const t = String(text || '').trim();
- if (!t) return { status: 'empty' };
- await publishLog({ text: t, label: String(label || '').trim(), mode: 'manual' });
- return { status: 'ok' };
- }
- function sigOf(label, text) {
- return `${String(label || '').trim()}||${String(text || '').trim().slice(0, 120)}`;
- }
- async function getProcessedState() {
- const items = await readAllLogMessages();
- const refs = new Set();
- const sigs = new Set();
- for (const it of items) {
- if (it.ref) refs.add(it.ref);
- sigs.add(sigOf(it.label, it.text));
- }
- return { refs, sigs };
- }
- async function createAI() {
- await openSsb();
- if (!isAImodOn()) return { status: 'ai_disabled' };
- const actions = await listAllUserActions();
- if (!actions.length) return { status: 'no_actions' };
- const state = await getProcessedState();
- const pending = actions.filter(a => a.key && !state.refs.has(a.key));
- if (!pending.length) return { status: 'no_new_actions' };
- const MAX_ACTIONS = 40;
- const slice = pending.slice(0, MAX_ACTIONS);
- let published = 0;
- let aiFails = 0;
- let aiDown = false;
- for (const a of slice) {
- let sentence = '';
- if (!aiDown) {
- sentence = await callAI(buildActionPrompt(a));
- if (!sentence) {
- aiFails++;
- if (aiFails >= 3) aiDown = true;
- } else {
- aiFails = 0;
- }
- }
- if (!sentence) sentence = buildFallbackSentence(a);
- if (!sentence) continue;
- const sig = sigOf(a.type, sentence);
- if (state.sigs.has(sig)) { state.refs.add(a.key); continue; }
- await publishLog({ text: sentence, label: a.type, mode: 'ai', ref: a.key });
- state.refs.add(a.key);
- state.sigs.add(sig);
- published++;
- await new Promise(r => setTimeout(r, 300));
- }
- if (!published) return { status: 'no_narrative' };
- return { status: 'ok', count: published };
- }
- async function readAllLogMessages() {
- const ssbClient = await openSsb();
- const raw = await new Promise((resolve, reject) =>
- pull(
- ssbClient.createLogStream({ reverse: false, limit: logLimit }),
- pull.collect((err, arr) => err ? reject(err) : resolve(arr))
- )
- );
- const items = [];
- const tombstoned = new Set();
- const replaced = new Map();
- for (const m of raw) {
- if (!m || !m.value) continue;
- const keyIn = m.key;
- const valueIn = m.value;
- const tsIn = m.timestamp || valueIn?.timestamp || Date.now();
- let dec;
- try {
- dec = ssbClient.private.unbox({ key: keyIn, value: valueIn, timestamp: tsIn });
- } catch { continue; }
- const v = dec?.value;
- const c = v?.content;
- if (!c) continue;
- if (v.author !== userId) continue;
- if (c.type === 'tombstone' && c.target) { tombstoned.add(c.target); continue; }
- if (c.type !== 'log') continue;
- if (c.replaces) replaced.set(c.replaces, dec.key || keyIn);
- items.push({
- key: dec.key || keyIn,
- author: v.author,
- ts: v.timestamp || tsIn,
- cycle: c.cycle || 0,
- createdAt: c.createdAt || new Date(v.timestamp || tsIn).toISOString(),
- text: String(c.text || ''),
- label: String(c.label || ''),
- mode: c.mode === 'ai' ? 'ai' : 'manual',
- replaces: c.replaces || null,
- ref: c.ref || null
- });
- }
- const survivors = items.filter(i => !tombstoned.has(i.key) && !replaced.has(i.key));
- survivors.sort((a, b) => b.ts - a.ts);
- return survivors;
- }
- async function listLogs(filter = 'today') {
- const items = await readAllLogMessages();
- const win = FILTER_WINDOWS[filter];
- if (win === null || win === undefined) return items;
- const cutoff = Date.now() - win;
- return items.filter(i => i.ts >= cutoff);
- }
- async function getLogById(id) {
- const items = await readAllLogMessages();
- return items.find(i => i.key === id) || null;
- }
- async function updateLog(id, { text, label, mode }) {
- const current = await getLogById(id);
- if (!current) return { status: 'not_found' };
- await republishLog({
- replaces: current.key,
- text: text !== undefined ? text : current.text,
- label: label !== undefined ? label : current.label,
- mode: mode || current.mode,
- cycle: current.cycle,
- createdAt: current.createdAt
- });
- return { status: 'ok' };
- }
- async function deleteLog(id) {
- const current = await getLogById(id);
- if (!current) return { status: 'not_found' };
- await publishTombstone(current.key);
- return { status: 'ok' };
- }
- async function countLogs() {
- const items = await readAllLogMessages();
- return items.length;
- }
- return {
- createManual,
- createAI,
- updateLog,
- deleteLog,
- getLogById,
- listLogs,
- countLogs,
- isAImodOn
- };
- };
|