reports_model.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. const pull = require('../server/node_modules/pull-stream');
  2. const { getConfig } = require('../configs/config-manager.js');
  3. const logLimit = getConfig().ssbLogStream?.limit || 1000;
  4. const normU = (v) => String(v || '').trim().toUpperCase();
  5. const normalizeStatus = (v) => normU(v).replace(/\s+/g, '_').replace(/-+/g, '_');
  6. const normalizeSeverity = (v) => String(v || '').trim().toLowerCase();
  7. const ensureArray = (v) => Array.isArray(v) ? v.filter(Boolean) : [];
  8. const trimStr = (v) => String(v || '').trim();
  9. const normalizeTemplate = (category, tpl) => {
  10. const cat = normU(category);
  11. const t = tpl && typeof tpl === 'object' ? tpl : {};
  12. const pick = (keys) => {
  13. const out = {};
  14. for (const k of keys) {
  15. const val = trimStr(t[k]);
  16. if (val) out[k] = val;
  17. }
  18. return out;
  19. };
  20. if (cat === 'BUGS') {
  21. const out = pick(['stepsToReproduce', 'expectedBehavior', 'actualBehavior', 'environment', 'reproduceRate']);
  22. if (out.reproduceRate) out.reproduceRate = normU(out.reproduceRate);
  23. return out;
  24. }
  25. if (cat === 'FEATURES') {
  26. return pick(['problemStatement', 'userStory', 'acceptanceCriteria']);
  27. }
  28. if (cat === 'ABUSE') {
  29. return pick(['whatHappened', 'reportedUser', 'evidenceLinks']);
  30. }
  31. if (cat === 'CONTENT') {
  32. return pick(['contentLocation', 'whyInappropriate', 'requestedAction', 'evidenceLinks']);
  33. }
  34. return {};
  35. };
  36. module.exports = ({ cooler }) => {
  37. let ssb;
  38. const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
  39. return {
  40. async createReport(title, description, category, image, tagsRaw = [], severity = 'low', template = {}) {
  41. const ssb = await openSsb();
  42. const userId = ssb.id;
  43. let blobId = null;
  44. if (image) {
  45. blobId = String(image).trim() || null;
  46. }
  47. const tags = Array.isArray(tagsRaw)
  48. ? tagsRaw.filter(Boolean)
  49. : String(tagsRaw || '').split(',').map(t => t.trim()).filter(Boolean);
  50. const cat = normU(category);
  51. const content = {
  52. type: 'report',
  53. title,
  54. description,
  55. category: cat,
  56. createdAt: new Date().toISOString(),
  57. author: userId,
  58. image: blobId,
  59. tags,
  60. confirmations: [],
  61. severity: normalizeSeverity(severity) || 'low',
  62. status: 'OPEN',
  63. template: normalizeTemplate(cat, template)
  64. };
  65. return new Promise((res, rej) => ssb.publish(content, (err, msg) => err ? rej(err) : res(msg)));
  66. },
  67. async updateReportById(id, updatedContent) {
  68. const ssb = await openSsb();
  69. const userId = ssb.id;
  70. const report = await new Promise((res, rej) =>
  71. ssb.get(id, (err, r) => err ? rej(new Error('Report not found')) : res(r))
  72. );
  73. if (report.content.author !== userId) throw new Error('Not the author');
  74. const tags = Object.prototype.hasOwnProperty.call(updatedContent, 'tags')
  75. ? String(updatedContent.tags || '').split(',').map(t => t.trim()).filter(Boolean)
  76. : ensureArray(report.content.tags);
  77. let blobId = report.content.image || null;
  78. if (updatedContent.image) {
  79. blobId = String(updatedContent.image).trim() || null;
  80. }
  81. const nextStatus = Object.prototype.hasOwnProperty.call(updatedContent, 'status')
  82. ? normalizeStatus(updatedContent.status)
  83. : normalizeStatus(report.content.status || 'OPEN');
  84. const nextSeverity = Object.prototype.hasOwnProperty.call(updatedContent, 'severity')
  85. ? (normalizeSeverity(updatedContent.severity) || 'low')
  86. : (normalizeSeverity(report.content.severity) || 'low');
  87. const nextCategory = Object.prototype.hasOwnProperty.call(updatedContent, 'category')
  88. ? normU(updatedContent.category)
  89. : normU(report.content.category);
  90. const confirmations = ensureArray(report.content.confirmations);
  91. const baseTemplate = Object.prototype.hasOwnProperty.call(updatedContent, 'template')
  92. ? updatedContent.template
  93. : (report.content.template || {});
  94. const nextTemplate = normalizeTemplate(nextCategory, baseTemplate);
  95. const updated = {
  96. ...report.content,
  97. ...updatedContent,
  98. type: 'report',
  99. replaces: id,
  100. image: blobId,
  101. tags,
  102. confirmations,
  103. severity: nextSeverity,
  104. status: nextStatus,
  105. category: nextCategory,
  106. template: nextTemplate,
  107. updatedAt: new Date().toISOString(),
  108. author: report.content.author
  109. };
  110. return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
  111. },
  112. async deleteReportById(id) {
  113. const ssb = await openSsb();
  114. const userId = ssb.id;
  115. const report = await new Promise((res, rej) =>
  116. ssb.get(id, (err, r) => err ? rej(new Error('Report not found')) : res(r))
  117. );
  118. if (report.content.author !== userId) throw new Error('Not the author');
  119. const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
  120. return new Promise((res, rej) => ssb.publish(tombstone, (err, result) => err ? rej(err) : res(result)));
  121. },
  122. async getReportById(id) {
  123. const ssb = await openSsb();
  124. const report = await new Promise((res, rej) =>
  125. ssb.get(id, (err, r) => err ? rej(new Error('Report not found')) : res(r))
  126. );
  127. const c = report.content || {};
  128. const cat = normU(c.category);
  129. return {
  130. id,
  131. ...c,
  132. category: cat,
  133. status: normalizeStatus(c.status || 'OPEN'),
  134. severity: normalizeSeverity(c.severity) || 'low',
  135. confirmations: ensureArray(c.confirmations),
  136. tags: ensureArray(c.tags),
  137. template: normalizeTemplate(cat, c.template || {})
  138. };
  139. },
  140. async confirmReportById(id) {
  141. const ssb = await openSsb();
  142. const userId = ssb.id;
  143. const report = await new Promise((res, rej) =>
  144. ssb.get(id, (err, r) => err ? rej(new Error('Report not found')) : res(r))
  145. );
  146. const confirmations = ensureArray(report.content.confirmations);
  147. if (confirmations.includes(userId)) throw new Error('Already confirmed');
  148. const cat = normU(report.content.category);
  149. const updated = {
  150. ...report.content,
  151. type: 'report',
  152. replaces: id,
  153. confirmations: [...confirmations, userId],
  154. updatedAt: new Date().toISOString(),
  155. status: normalizeStatus(report.content.status || 'OPEN'),
  156. category: cat,
  157. severity: normalizeSeverity(report.content.severity) || 'low',
  158. template: normalizeTemplate(cat, report.content.template || {})
  159. };
  160. return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
  161. },
  162. async listAll() {
  163. const ssb = await openSsb();
  164. return new Promise((resolve, reject) => {
  165. pull(
  166. ssb.createLogStream({ limit: logLimit }),
  167. pull.collect((err, results) => {
  168. if (err) return reject(err);
  169. const tombstoned = new Set();
  170. const replaced = new Map();
  171. const reports = new Map();
  172. for (const r of results) {
  173. const key = r && r.key;
  174. const c = r && r.value && r.value.content ? r.value.content : null;
  175. if (!key || !c) continue;
  176. if (c.type === 'tombstone' && c.target) tombstoned.add(c.target);
  177. if (c.type === 'report') {
  178. if (c.replaces) replaced.set(c.replaces, key);
  179. const cat = normU(c.category);
  180. reports.set(key, {
  181. id: key,
  182. ...c,
  183. category: cat,
  184. status: normalizeStatus(c.status || 'OPEN'),
  185. severity: normalizeSeverity(c.severity) || 'low',
  186. confirmations: ensureArray(c.confirmations),
  187. tags: ensureArray(c.tags),
  188. template: normalizeTemplate(cat, c.template || {})
  189. });
  190. }
  191. }
  192. tombstoned.forEach(id => reports.delete(id));
  193. replaced.forEach((_, oldId) => reports.delete(oldId));
  194. resolve([...reports.values()]);
  195. })
  196. );
  197. });
  198. }
  199. };
  200. };