bookmarking_model.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. const pull = require('../server/node_modules/pull-stream')
  2. const moment = require('../server/node_modules/moment')
  3. module.exports = ({ cooler }) => {
  4. let ssb
  5. const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb }
  6. return {
  7. type: 'bookmark',
  8. async createBookmark(url, tagsRaw, description, category, lastVisit) {
  9. const ssbClient = await openSsb()
  10. const userId = ssbClient.id
  11. let tags = Array.isArray(tagsRaw) ? tagsRaw.filter(t => t) : tagsRaw.split(',').map(t => t.trim()).filter(Boolean)
  12. const isInternal = url.includes('127.0.0.1') || url.includes('localhost')
  13. if (!tags.includes(isInternal ? 'internal' : 'external')) {
  14. tags.push(isInternal ? 'internal' : 'external')
  15. }
  16. const formattedLastVisit = lastVisit
  17. ? moment(lastVisit, moment.ISO_8601, true).toISOString()
  18. : moment().toISOString()
  19. const content = {
  20. type: 'bookmark',
  21. author: userId,
  22. url,
  23. tags,
  24. description,
  25. category,
  26. createdAt: new Date().toISOString(),
  27. updatedAt: new Date().toISOString(),
  28. lastVisit: formattedLastVisit,
  29. opinions: {},
  30. opinions_inhabitants: []
  31. }
  32. return new Promise((resolve, reject) => {
  33. ssbClient.publish(content, (err, res) => err ? reject(new Error("Error creating bookmark: " + err.message)) : resolve(res))
  34. })
  35. },
  36. async listAll(author = null, filter = 'all') {
  37. const ssbClient = await openSsb()
  38. const userId = ssbClient.id
  39. const results = await new Promise((res, rej) => {
  40. pull(
  41. ssbClient.createLogStream(),
  42. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  43. )
  44. })
  45. const tombstoned = new Set(
  46. results
  47. .filter(m => m.value.content?.type === 'tombstone')
  48. .map(m => m.value.content.target)
  49. )
  50. const replaces = new Map()
  51. const latest = new Map()
  52. for (const m of results) {
  53. const k = m.key
  54. const c = m.value.content
  55. if (!c || c.type !== 'bookmark') continue
  56. if (tombstoned.has(k)) continue
  57. if (c.replaces) replaces.set(c.replaces, k)
  58. latest.set(k, {
  59. id: k,
  60. url: c.url,
  61. description: c.description,
  62. category: c.category,
  63. createdAt: c.createdAt,
  64. lastVisit: c.lastVisit,
  65. tags: c.tags || [],
  66. opinions: c.opinions || {},
  67. opinions_inhabitants: c.opinions_inhabitants || [],
  68. author: c.author
  69. })
  70. }
  71. for (const oldId of replaces.keys()) {
  72. latest.delete(oldId)
  73. }
  74. let bookmarks = Array.from(latest.values())
  75. if (filter === 'mine' && author === userId) {
  76. bookmarks = bookmarks.filter(b => b.author === author)
  77. } else if (filter === 'external') {
  78. bookmarks = bookmarks.filter(b => b.tags.includes('external'))
  79. } else if (filter === 'internal') {
  80. bookmarks = bookmarks.filter(b => b.tags.includes('internal'))
  81. }
  82. return bookmarks
  83. },
  84. async updateBookmarkById(bookmarkId, updatedData) {
  85. const ssbClient = await openSsb()
  86. const userId = ssbClient.id
  87. const old = await new Promise((res, rej) =>
  88. ssbClient.get(bookmarkId, (err, msg) =>
  89. err || !msg?.content ? rej(err || new Error("Error retrieving old bookmark.")) : res(msg)
  90. )
  91. )
  92. if (Object.keys(old.content.opinions || {}).length > 0) {
  93. throw new Error('Cannot edit bookmark after it has received opinions.')
  94. }
  95. const tags = updatedData.tags
  96. ? updatedData.tags.split(',').map(t => t.trim()).filter(Boolean)
  97. : []
  98. const isInternal = updatedData.url.includes('127.0.0.1') || updatedData.url.includes('localhost')
  99. if (!tags.includes(isInternal ? 'internal' : 'external')) {
  100. tags.push(isInternal ? 'internal' : 'external')
  101. }
  102. const formattedLastVisit = updatedData.lastVisit
  103. ? moment(updatedData.lastVisit, moment.ISO_8601, true).toISOString()
  104. : moment().toISOString()
  105. const updated = {
  106. type: 'bookmark',
  107. replaces: bookmarkId,
  108. author: old.content.author,
  109. url: updatedData.url,
  110. tags,
  111. description: updatedData.description,
  112. category: updatedData.category,
  113. createdAt: old.content.createdAt,
  114. updatedAt: new Date().toISOString(),
  115. lastVisit: formattedLastVisit,
  116. opinions: old.content.opinions,
  117. opinions_inhabitants: old.content.opinions_inhabitants
  118. }
  119. return new Promise((resolve, reject) => {
  120. ssbClient.publish(updated, (err2, res) => err2 ? reject(new Error("Error creating updated bookmark.")) : resolve(res))
  121. })
  122. },
  123. async deleteBookmarkById(bookmarkId) {
  124. const ssbClient = await openSsb()
  125. const userId = ssbClient.id
  126. const msg = await new Promise((res, rej) =>
  127. ssbClient.get(bookmarkId, (err, m) => err ? rej(new Error("Error retrieving bookmark.")) : res(m))
  128. )
  129. if (msg.content.author !== userId) throw new Error("Error: You are not the author of this bookmark.")
  130. const tombstone = {
  131. type: 'tombstone',
  132. target: bookmarkId,
  133. deletedAt: new Date().toISOString(),
  134. author: userId
  135. }
  136. return new Promise((resolve, reject) => {
  137. ssbClient.publish(tombstone, (err2, res) => {
  138. if (err2) return reject(new Error("Error creating tombstone."))
  139. resolve(res)
  140. })
  141. })
  142. },
  143. async getBookmarkById(bookmarkId) {
  144. const ssbClient = await openSsb()
  145. return new Promise((resolve, reject) => {
  146. ssbClient.get(bookmarkId, (err, msg) => {
  147. if (err || !msg || !msg.content) return reject(new Error("Error retrieving bookmark"))
  148. const c = msg.content
  149. resolve({
  150. id: bookmarkId,
  151. url: c.url || "Unknown",
  152. description: c.description || "No description",
  153. category: c.category || "No category",
  154. createdAt: c.createdAt || "Unknown",
  155. updatedAt: c.updatedAt || "Unknown",
  156. lastVisit: c.lastVisit || "Unknown",
  157. tags: c.tags || [],
  158. opinions: c.opinions || {},
  159. opinions_inhabitants: c.opinions_inhabitants || [],
  160. author: c.author || "Unknown"
  161. })
  162. })
  163. })
  164. },
  165. async createOpinion(bookmarkId, category) {
  166. const ssbClient = await openSsb()
  167. const userId = ssbClient.id
  168. return new Promise((resolve, reject) => {
  169. ssbClient.get(bookmarkId, (err, msg) => {
  170. if (err || !msg || msg.content?.type !== 'bookmark') return reject(new Error('Bookmark not found'))
  171. if (msg.content.opinions_inhabitants?.includes(userId)) return reject(new Error('Already voted'))
  172. const updated = {
  173. ...msg.content,
  174. replaces: bookmarkId,
  175. opinions: {
  176. ...msg.content.opinions,
  177. [category]: (msg.content.opinions?.[category] || 0) + 1
  178. },
  179. opinions_inhabitants: [...(msg.content.opinions_inhabitants || []), userId],
  180. updatedAt: new Date().toISOString()
  181. }
  182. ssbClient.publish(updated, (err3, result) => err3 ? reject(err3) : resolve(result))
  183. })
  184. })
  185. }
  186. }
  187. }