images_model.js 7.4 KB

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