videos_model.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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. const openSsb = async () => {
  8. if (!ssb) ssb = await cooler.open();
  9. return ssb;
  10. };
  11. return {
  12. async createVideo(blobMarkdown, tagsRaw, title, description) {
  13. const ssbClient = await openSsb();
  14. const userId = ssbClient.id;
  15. const match = blobMarkdown?.match(/\(([^)]+)\)/);
  16. const blobId = match ? match[1] : blobMarkdown;
  17. const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean) : [];
  18. const content = {
  19. type: 'video',
  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 updateVideoById(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 !== 'video') return reject(new Error('Video not found'));
  39. if (Object.keys(oldMsg.content.opinions || {}).length > 0) return reject(new Error('Cannot edit video after it has received opinions.'));
  40. if (oldMsg.content.author !== userId) return reject(new Error('Not the author'));
  41. const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean) : oldMsg.content.tags;
  42. const match = blobMarkdown?.match(/\(([^)]+)\)/);
  43. const blobId = match ? match[1] : blobMarkdown;
  44. const tombstone = {
  45. type: 'tombstone',
  46. target: id,
  47. deletedAt: new Date().toISOString(),
  48. author: userId
  49. };
  50. const updated = {
  51. ...oldMsg.content,
  52. url: blobId || oldMsg.content.url,
  53. tags,
  54. title: title || '',
  55. description: description || '',
  56. updatedAt: new Date().toISOString(),
  57. replaces: id
  58. };
  59. ssbClient.publish(tombstone, err1 => {
  60. if (err1) return reject(err1);
  61. ssbClient.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result));
  62. });
  63. });
  64. });
  65. },
  66. async deleteVideoById(id) {
  67. const ssbClient = await openSsb();
  68. const userId = ssbClient.id;
  69. return new Promise((resolve, reject) => {
  70. ssbClient.get(id, (err, msg) => {
  71. if (err || !msg || msg.content?.type !== 'video') return reject(new Error('Video not found'));
  72. if (msg.content.author !== userId) return reject(new Error('Not the author'));
  73. const tombstone = {
  74. type: 'tombstone',
  75. target: id,
  76. deletedAt: new Date().toISOString(),
  77. author: userId
  78. };
  79. ssbClient.publish(tombstone, (err2, res) => err2 ? reject(err2) : resolve(res));
  80. });
  81. });
  82. },
  83. async listAll(filter = 'all') {
  84. const ssbClient = await openSsb();
  85. const userId = ssbClient.id;
  86. const messages = await new Promise((res, rej) => {
  87. pull(
  88. ssbClient.createLogStream({ limit: logLimit }),
  89. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  90. );
  91. });
  92. const tombstoned = new Set();
  93. const replaces = new Map();
  94. const videos = new Map();
  95. for (const m of messages) {
  96. const k = m.key;
  97. const c = m.value.content;
  98. if (!c) continue;
  99. if (c.type === 'tombstone' && c.target) {
  100. tombstoned.add(c.target);
  101. continue;
  102. }
  103. if (c.type !== 'video') continue;
  104. if (c.replaces) replaces.set(c.replaces, k);
  105. videos.set(k, {
  106. key: k,
  107. url: c.url,
  108. createdAt: c.createdAt,
  109. updatedAt: c.updatedAt || null,
  110. tags: c.tags || [],
  111. author: c.author,
  112. title: c.title || '',
  113. description: c.description || '',
  114. opinions: c.opinions || {},
  115. opinions_inhabitants: c.opinions_inhabitants || []
  116. });
  117. }
  118. for (const oldId of replaces.keys()) videos.delete(oldId);
  119. for (const delId of tombstoned.values()) videos.delete(delId);
  120. let out = Array.from(videos.values());
  121. if (filter === 'mine') {
  122. out = out.filter(v => v.author === userId);
  123. } else if (filter === 'recent') {
  124. const now = Date.now();
  125. out = out.filter(v => new Date(v.createdAt).getTime() >= now - 86400000);
  126. } else if (filter === 'top') {
  127. out = out.sort((a, b) => {
  128. const sum = o => Object.values(o || {}).reduce((s, n) => s + (n || 0), 0);
  129. return sum(b.opinions) - sum(a.opinions);
  130. });
  131. } else {
  132. out = out.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  133. }
  134. return out;
  135. },
  136. async getVideoById(id) {
  137. const ssbClient = await openSsb();
  138. return new Promise((resolve, reject) => {
  139. ssbClient.get(id, (err, msg) => {
  140. if (err || !msg || msg.content?.type !== 'video') return reject(new Error('Video not found'));
  141. resolve({
  142. key: id,
  143. url: msg.content.url,
  144. createdAt: msg.content.createdAt,
  145. updatedAt: msg.content.updatedAt || null,
  146. tags: msg.content.tags || [],
  147. author: msg.content.author,
  148. title: msg.content.title || '',
  149. description: msg.content.description || '',
  150. opinions: msg.content.opinions || {},
  151. opinions_inhabitants: msg.content.opinions_inhabitants || []
  152. });
  153. });
  154. });
  155. },
  156. async createOpinion(id, category) {
  157. if (!categories.includes(category)) return Promise.reject(new Error('Invalid voting category'));
  158. const ssbClient = await openSsb();
  159. const userId = ssbClient.id;
  160. return new Promise((resolve, reject) => {
  161. ssbClient.get(id, (err, msg) => {
  162. if (err || !msg || msg.content?.type !== 'video') return reject(new Error('Video not found'));
  163. if (msg.content.opinions_inhabitants?.includes(userId)) return reject(new Error('Already voted'));
  164. const updated = {
  165. ...msg.content,
  166. replaces: id,
  167. opinions: {
  168. ...msg.content.opinions,
  169. [category]: (msg.content.opinions?.[category] || 0) + 1
  170. },
  171. opinions_inhabitants: [...(msg.content.opinions_inhabitants || []), userId],
  172. updatedAt: new Date().toISOString()
  173. };
  174. ssbClient.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result));
  175. });
  176. });
  177. }
  178. };
  179. };