documents_model.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. const pull = require('../server/node_modules/pull-stream')
  2. const extractBlobId = str => {
  3. if (!str || typeof str !== 'string') return null
  4. const match = str.match(/\(([^)]+\.sha256)\)/)
  5. return match ? match[1] : str.trim()
  6. }
  7. const parseCSV = str => str ? str.split(',').map(s => s.trim()).filter(Boolean) : []
  8. module.exports = ({ cooler }) => {
  9. let ssb
  10. const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
  11. return {
  12. type: 'document',
  13. async createDocument(blobMarkdown, tagsRaw, title, description) {
  14. const ssbClient = await openSsb()
  15. const userId = ssbClient.id
  16. const blobId = extractBlobId(blobMarkdown)
  17. const tags = parseCSV(tagsRaw)
  18. const content = {
  19. type: 'document',
  20. url: blobId,
  21. createdAt: new Date().toISOString(),
  22. author: userId,
  23. tags,
  24. title: title || '',
  25. description: description || '',
  26. opinions: {},
  27. opinions_inhabitants: []
  28. }
  29. return new Promise((resolve, reject) => {
  30. ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res))
  31. })
  32. },
  33. async updateDocumentById(id, blobMarkdown, tagsRaw, title, description) {
  34. const ssbClient = await openSsb()
  35. const userId = ssbClient.id
  36. return new Promise((resolve, reject) => {
  37. ssbClient.get(id, (err, oldMsg) => {
  38. if (err || !oldMsg || oldMsg.content?.type !== 'document') return reject(new Error('Document not found'))
  39. if (Object.keys(oldMsg.content.opinions || {}).length > 0) return reject(new Error('Cannot edit document after it has received opinions.'))
  40. if (oldMsg.content.author !== userId) return reject(new Error('Not the author'))
  41. const tags = parseCSV(tagsRaw)
  42. const blobId = extractBlobId(blobMarkdown)
  43. const updated = {
  44. ...oldMsg.content,
  45. replaces: id,
  46. url: blobId || oldMsg.content.url,
  47. tags,
  48. title: title || '',
  49. description: description || '',
  50. updatedAt: new Date().toISOString()
  51. }
  52. ssbClient.publish(updated, (err3, result) => err3 ? reject(err3) : resolve(result))
  53. })
  54. })
  55. },
  56. async deleteDocumentById(id) {
  57. const ssbClient = await openSsb()
  58. const userId = ssbClient.id
  59. return new Promise((resolve, reject) => {
  60. ssbClient.get(id, (err, msg) => {
  61. if (err || !msg || msg.content?.type !== 'document') return reject(new Error('Document not found'))
  62. if (msg.content.author !== userId) return reject(new Error('Not the author'))
  63. const tombstone = {
  64. type: 'tombstone',
  65. target: id,
  66. deletedAt: new Date().toISOString(),
  67. author: userId
  68. }
  69. ssbClient.publish(tombstone, (err2, res) => err2 ? reject(err2) : resolve(res))
  70. })
  71. })
  72. },
  73. async listAll(filter = 'all') {
  74. const ssbClient = await openSsb()
  75. const userId = ssbClient.id
  76. const messages = await new Promise((res, rej) => {
  77. pull(
  78. ssbClient.createLogStream(),
  79. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  80. )
  81. })
  82. const tombstoned = new Set(
  83. messages
  84. .filter(m => m.value.content?.type === 'tombstone')
  85. .map(m => m.value.content.target)
  86. )
  87. const replaces = new Map()
  88. const latest = new Map()
  89. for (const m of messages) {
  90. const k = m.key
  91. const c = m.value?.content
  92. if (!c || c.type !== 'document') continue
  93. if (tombstoned.has(k)) continue
  94. if (c.replaces) replaces.set(c.replaces, k)
  95. latest.set(k, {
  96. key: k,
  97. url: c.url,
  98. createdAt: c.createdAt,
  99. updatedAt: c.updatedAt || null,
  100. tags: c.tags || [],
  101. author: c.author,
  102. title: c.title || '',
  103. description: c.description || '',
  104. opinions: c.opinions || {},
  105. opinions_inhabitants: c.opinions_inhabitants || []
  106. })
  107. }
  108. for (const oldId of replaces.keys()) {
  109. latest.delete(oldId)
  110. }
  111. let documents = Array.from(latest.values())
  112. if (filter === 'mine') {
  113. documents = documents.filter(d => d.author === userId)
  114. } else {
  115. documents = documents.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
  116. }
  117. const hasBlob = (blobId) => {
  118. return new Promise((resolve) => {
  119. ssbClient.blobs.has(blobId, (err, has) => {
  120. resolve(!err && has);
  121. });
  122. });
  123. };
  124. documents = await Promise.all(
  125. documents.map(async (doc) => {
  126. const ok = await hasBlob(doc.url);
  127. return ok ? doc : null;
  128. })
  129. );
  130. documents = documents.filter(Boolean);
  131. return documents
  132. },
  133. async getDocumentById(id) {
  134. const ssbClient = await openSsb()
  135. return new Promise((resolve, reject) => {
  136. ssbClient.get(id, (err, msg) => {
  137. if (err || !msg || msg.content?.type !== 'document') return reject(new Error('Document not found'))
  138. resolve({
  139. key: id,
  140. url: msg.content.url,
  141. createdAt: msg.content.createdAt,
  142. updatedAt: msg.content.updatedAt || null,
  143. tags: msg.content.tags || [],
  144. author: msg.content.author,
  145. title: msg.content.title || '',
  146. description: msg.content.description || '',
  147. opinions: msg.content.opinions || {},
  148. opinions_inhabitants: msg.content.opinions_inhabitants || []
  149. })
  150. })
  151. })
  152. },
  153. async createOpinion(id, category) {
  154. const ssbClient = await openSsb()
  155. const userId = ssbClient.id
  156. return new Promise((resolve, reject) => {
  157. ssbClient.get(id, (err, msg) => {
  158. if (err || !msg || msg.content?.type !== 'document') return reject(new Error('Document not found'))
  159. if (msg.content.opinions_inhabitants?.includes(userId)) return reject(new Error('Already voted'))
  160. const updated = {
  161. ...msg.content,
  162. replaces: id,
  163. opinions: {
  164. ...msg.content.opinions,
  165. [category]: (msg.content.opinions?.[category] || 0) + 1
  166. },
  167. opinions_inhabitants: [...(msg.content.opinions_inhabitants || []), userId],
  168. updatedAt: new Date().toISOString()
  169. }
  170. ssbClient.publish(updated, (err3, result) => err3 ? reject(err3) : resolve(result))
  171. })
  172. })
  173. }
  174. }
  175. }