logs_model.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. const pull = require('../server/node_modules/pull-stream');
  2. const util = require('../server/node_modules/util');
  3. const axios = require('../server/node_modules/axios');
  4. const fs = require('fs');
  5. const path = require('path');
  6. const { getConfig } = require('../configs/config-manager.js');
  7. const logLimit = getConfig().ssbLogStream?.limit || 1000;
  8. const CYCLE_PATH = path.join(__dirname, '..', 'configs', 'blockchain-cycle.json');
  9. const readCycle = () => {
  10. try { return JSON.parse(fs.readFileSync(CYCLE_PATH, 'utf8')).cycle || 0; }
  11. catch { return 0; }
  12. };
  13. const DAY_MS = 24 * 60 * 60 * 1000;
  14. const WEEK_MS = 7 * DAY_MS;
  15. const MONTH_MS = 30 * DAY_MS;
  16. const YEAR_MS = 365 * DAY_MS;
  17. const FILTER_WINDOWS = {
  18. today: DAY_MS,
  19. week: WEEK_MS,
  20. month: MONTH_MS,
  21. year: YEAR_MS,
  22. always: null
  23. };
  24. const ACTION_TYPES = new Set([
  25. 'post', 'about', 'contact', 'feed', 'bookmark', 'image', 'audio', 'video',
  26. 'document', 'torrent', 'event', 'task', 'taskAssignment',
  27. 'votes', 'vote', 'report', 'tribe', 'chat', 'chatMessage', 'pad', 'padEntry',
  28. 'forum', 'market', 'job', 'project', 'pixelia', 'map', 'mapMarker',
  29. 'shop', 'shopProduct', 'curriculum', 'gameScore',
  30. 'calendar', 'calendarDate', 'calendarNote',
  31. 'transfer', 'bankClaim', 'ubiClaim',
  32. 'parliamentCandidature', 'parliamentProposal', 'parliamentLaw',
  33. 'parliamentTerm', 'parliamentRevocation',
  34. 'courtsCase', 'courtsEvidence', 'courtsAnswer', 'courtsVerdict',
  35. 'courtsNomination', 'courtsNominationVote',
  36. 'courtsSettlementProposal', 'courtsSettlementAccepted',
  37. 'tribeParliamentCandidature', 'tribeParliamentRule'
  38. ]);
  39. const ACTION_PHRASES = {
  40. post: 'published a post',
  41. about: 'updated profile information',
  42. contact: 'followed or unfollowed someone',
  43. feed: 'shared content in the feed',
  44. bookmark: 'bookmarked a resource',
  45. image: 'uploaded an image',
  46. audio: 'uploaded an audio track',
  47. video: 'uploaded a video',
  48. document: 'uploaded a document',
  49. torrent: 'shared a torrent',
  50. event: 'created an event',
  51. task: 'created a task',
  52. taskAssignment: 'updated a task assignment',
  53. votes: 'participated in a vote',
  54. vote: 'cast a vote',
  55. report: 'submitted a report',
  56. tribe: 'interacted with a tribe',
  57. chat: 'opened a chat room',
  58. chatMessage: 'sent a chat message',
  59. pad: 'worked on a collaborative pad',
  60. padEntry: 'edited a pad entry',
  61. market: 'posted in the market',
  62. forum: 'posted in the forum',
  63. job: 'posted a job opportunity',
  64. project: 'advanced a project',
  65. pixelia: 'placed a pixel in pixelia',
  66. map: 'contributed to a map',
  67. mapMarker: 'placed a marker on a map',
  68. shop: 'updated a shop',
  69. shopProduct: 'managed a shop product',
  70. curriculum: 'edited the curriculum',
  71. gameScore: 'logged a game score',
  72. calendar: 'managed a calendar',
  73. calendarDate: 'added a calendar date',
  74. calendarNote: 'added a calendar note',
  75. transfer: 'sent or confirmed a transfer',
  76. bankClaim: 'completed a banking claim',
  77. ubiClaim: 'claimed the UBI',
  78. parliamentCandidature: 'published a parliamentary candidature',
  79. parliamentProposal: 'published a parliamentary proposal',
  80. parliamentLaw: 'participated in a parliamentary law',
  81. parliamentTerm: 'participated in a parliamentary term',
  82. parliamentRevocation: 'submitted a parliamentary revocation',
  83. courtsCase: 'opened a courts case',
  84. courtsEvidence: 'submitted courts evidence',
  85. courtsAnswer: 'replied in a courts case',
  86. courtsVerdict: 'reached a courts verdict',
  87. courtsNomination: 'nominated a judge',
  88. courtsNominationVote: 'voted on a judge nomination',
  89. courtsSettlementProposal: 'proposed a courts settlement',
  90. courtsSettlementAccepted: 'accepted a courts settlement',
  91. tribeParliamentCandidature: 'stood for a tribe parliament',
  92. tribeParliamentRule: 'contributed a tribe parliament rule'
  93. };
  94. const compact = (s, n = 200) => String(s || '').replace(/\s+/g, ' ').trim().slice(0, n);
  95. module.exports = ({ cooler }) => {
  96. let ssb;
  97. let userId;
  98. const openSsb = async () => {
  99. if (!ssb) {
  100. ssb = await cooler.open();
  101. userId = ssb.id;
  102. }
  103. return ssb;
  104. };
  105. async function listAllUserActions() {
  106. const ssbClient = await openSsb();
  107. const msgs = await new Promise((resolve, reject) =>
  108. pull(
  109. ssbClient.createLogStream({ reverse: true, limit: logLimit }),
  110. pull.collect((err, arr) => err ? reject(err) : resolve(arr))
  111. )
  112. );
  113. const out = [];
  114. for (const m of msgs) {
  115. const v = m?.value || {};
  116. const c = v?.content;
  117. if (!c || typeof c !== 'object' || !c.type) continue;
  118. if (v.author !== userId) continue;
  119. if (c.type === 'log') continue;
  120. if (!ACTION_TYPES.has(c.type)) continue;
  121. const ts = v.timestamp || 0;
  122. const summary = c.title || c.text || c.question || c.subject || c.name || c.concept || c.description || '';
  123. out.push({ key: m.key, ts, type: c.type, summary: compact(summary) });
  124. }
  125. return out;
  126. }
  127. async function callAI(prompt) {
  128. if (!prompt) return '';
  129. const tryOnce = async () => {
  130. try {
  131. const res = await axios.post('http://localhost:4001/ai', { input: prompt, raw: true }, { timeout: 90000 });
  132. return String(res?.data?.answer || '').trim();
  133. } catch { return ''; }
  134. };
  135. let out = await tryOnce();
  136. if (!out) {
  137. await new Promise(r => setTimeout(r, 2000));
  138. out = await tryOnce();
  139. }
  140. return out;
  141. }
  142. function buildActionPrompt(a) {
  143. const d = new Date(a.ts).toISOString().slice(0, 16).replace('T', ' ');
  144. const ctx = a.summary ? ` Subject: "${compact(a.summary, 120)}".` : '';
  145. return `One first-person diary sentence about a "${a.type}" action at ${d}.${ctx} Vary phrasing. No IDs, hashes, quotes, lists or markdown.`;
  146. }
  147. function buildFallbackSentence(a) {
  148. const phrase = ACTION_PHRASES[a.type] || `performed a ${a.type} action`;
  149. const d = new Date(a.ts).toISOString().slice(0, 16).replace('T', ' ');
  150. const ctx = a.summary ? ` — ${compact(a.summary, 120)}` : '';
  151. return `At ${d} I ${phrase}${ctx}.`;
  152. }
  153. function isAImodOn() {
  154. try { return getConfig().modules?.aiMod === 'on'; } catch { return false; }
  155. }
  156. async function publishLog({ text, label, mode, ref }) {
  157. const ssbClient = await openSsb();
  158. const content = {
  159. type: 'log',
  160. text: String(text || '').slice(0, 8000),
  161. label: String(label || '').slice(0, 200),
  162. mode: mode === 'ai' ? 'ai' : 'manual',
  163. cycle: readCycle(),
  164. createdAt: new Date().toISOString(),
  165. timestamp: Date.now(),
  166. private: true
  167. };
  168. if (ref) content.ref = String(ref);
  169. const publishAsync = util.promisify(ssbClient.private.publish);
  170. return publishAsync(content, [userId]);
  171. }
  172. async function republishLog({ replaces, text, label, mode, cycle, createdAt }) {
  173. const ssbClient = await openSsb();
  174. const content = {
  175. type: 'log',
  176. replaces,
  177. text: String(text || '').slice(0, 8000),
  178. label: String(label || '').slice(0, 200),
  179. mode: mode === 'ai' ? 'ai' : 'manual',
  180. cycle: cycle || readCycle(),
  181. createdAt: createdAt || new Date().toISOString(),
  182. updatedAt: new Date().toISOString(),
  183. timestamp: Date.now(),
  184. private: true
  185. };
  186. const publishAsync = util.promisify(ssbClient.private.publish);
  187. return publishAsync(content, [userId]);
  188. }
  189. async function publishTombstone(target) {
  190. const ssbClient = await openSsb();
  191. const content = {
  192. type: 'tombstone',
  193. target,
  194. deletedAt: new Date().toISOString(),
  195. author: userId,
  196. private: true
  197. };
  198. const publishAsync = util.promisify(ssbClient.private.publish);
  199. return publishAsync(content, [userId]);
  200. }
  201. async function createManual(label, text) {
  202. await openSsb();
  203. const t = String(text || '').trim();
  204. if (!t) return { status: 'empty' };
  205. await publishLog({ text: t, label: String(label || '').trim(), mode: 'manual' });
  206. return { status: 'ok' };
  207. }
  208. function sigOf(label, text) {
  209. return `${String(label || '').trim()}||${String(text || '').trim().slice(0, 120)}`;
  210. }
  211. async function getProcessedState() {
  212. const items = await readAllLogMessages();
  213. const refs = new Set();
  214. const sigs = new Set();
  215. for (const it of items) {
  216. if (it.ref) refs.add(it.ref);
  217. sigs.add(sigOf(it.label, it.text));
  218. }
  219. return { refs, sigs };
  220. }
  221. async function createAI() {
  222. await openSsb();
  223. if (!isAImodOn()) return { status: 'ai_disabled' };
  224. const actions = await listAllUserActions();
  225. if (!actions.length) return { status: 'no_actions' };
  226. const state = await getProcessedState();
  227. const pending = actions.filter(a => a.key && !state.refs.has(a.key));
  228. if (!pending.length) return { status: 'no_new_actions' };
  229. const MAX_ACTIONS = 40;
  230. const slice = pending.slice(0, MAX_ACTIONS);
  231. let published = 0;
  232. let aiFails = 0;
  233. let aiDown = false;
  234. for (const a of slice) {
  235. let sentence = '';
  236. if (!aiDown) {
  237. sentence = await callAI(buildActionPrompt(a));
  238. if (!sentence) {
  239. aiFails++;
  240. if (aiFails >= 3) aiDown = true;
  241. } else {
  242. aiFails = 0;
  243. }
  244. }
  245. if (!sentence) sentence = buildFallbackSentence(a);
  246. if (!sentence) continue;
  247. const sig = sigOf(a.type, sentence);
  248. if (state.sigs.has(sig)) { state.refs.add(a.key); continue; }
  249. await publishLog({ text: sentence, label: a.type, mode: 'ai', ref: a.key });
  250. state.refs.add(a.key);
  251. state.sigs.add(sig);
  252. published++;
  253. await new Promise(r => setTimeout(r, 300));
  254. }
  255. if (!published) return { status: 'no_narrative' };
  256. return { status: 'ok', count: published };
  257. }
  258. async function readAllLogMessages() {
  259. const ssbClient = await openSsb();
  260. const raw = await new Promise((resolve, reject) =>
  261. pull(
  262. ssbClient.createLogStream({ reverse: false, limit: logLimit }),
  263. pull.collect((err, arr) => err ? reject(err) : resolve(arr))
  264. )
  265. );
  266. const items = [];
  267. const tombstoned = new Set();
  268. const replaced = new Map();
  269. for (const m of raw) {
  270. if (!m || !m.value) continue;
  271. const keyIn = m.key;
  272. const valueIn = m.value;
  273. const tsIn = m.timestamp || valueIn?.timestamp || Date.now();
  274. let dec;
  275. try {
  276. dec = ssbClient.private.unbox({ key: keyIn, value: valueIn, timestamp: tsIn });
  277. } catch { continue; }
  278. const v = dec?.value;
  279. const c = v?.content;
  280. if (!c) continue;
  281. if (v.author !== userId) continue;
  282. if (c.type === 'tombstone' && c.target) { tombstoned.add(c.target); continue; }
  283. if (c.type !== 'log') continue;
  284. if (c.replaces) replaced.set(c.replaces, dec.key || keyIn);
  285. items.push({
  286. key: dec.key || keyIn,
  287. author: v.author,
  288. ts: v.timestamp || tsIn,
  289. cycle: c.cycle || 0,
  290. createdAt: c.createdAt || new Date(v.timestamp || tsIn).toISOString(),
  291. text: String(c.text || ''),
  292. label: String(c.label || ''),
  293. mode: c.mode === 'ai' ? 'ai' : 'manual',
  294. replaces: c.replaces || null,
  295. ref: c.ref || null
  296. });
  297. }
  298. const survivors = items.filter(i => !tombstoned.has(i.key) && !replaced.has(i.key));
  299. survivors.sort((a, b) => b.ts - a.ts);
  300. return survivors;
  301. }
  302. async function listLogs(filter = 'today') {
  303. const items = await readAllLogMessages();
  304. const win = FILTER_WINDOWS[filter];
  305. if (win === null || win === undefined) return items;
  306. const cutoff = Date.now() - win;
  307. return items.filter(i => i.ts >= cutoff);
  308. }
  309. async function getLogById(id) {
  310. const items = await readAllLogMessages();
  311. return items.find(i => i.key === id) || null;
  312. }
  313. async function updateLog(id, { text, label, mode }) {
  314. const current = await getLogById(id);
  315. if (!current) return { status: 'not_found' };
  316. await republishLog({
  317. replaces: current.key,
  318. text: text !== undefined ? text : current.text,
  319. label: label !== undefined ? label : current.label,
  320. mode: mode || current.mode,
  321. cycle: current.cycle,
  322. createdAt: current.createdAt
  323. });
  324. return { status: 'ok' };
  325. }
  326. async function deleteLog(id) {
  327. const current = await getLogById(id);
  328. if (!current) return { status: 'not_found' };
  329. await publishTombstone(current.key);
  330. return { status: 'ok' };
  331. }
  332. async function countLogs() {
  333. const items = await readAllLogMessages();
  334. return items.length;
  335. }
  336. return {
  337. createManual,
  338. createAI,
  339. updateLog,
  340. deleteLog,
  341. getLogById,
  342. listLogs,
  343. countLogs,
  344. isAImodOn
  345. };
  346. };