videos_model.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. const pull = require('../server/node_modules/pull-stream');
  2. module.exports = ({ cooler }) => {
  3. let ssb;
  4. const openSsb = async () => {
  5. if (!ssb) ssb = await cooler.open();
  6. return ssb;
  7. };
  8. return {
  9. async createVideo(blobMarkdown, tagsRaw, title, description) {
  10. const ssbClient = await openSsb();
  11. const userId = ssbClient.id;
  12. const match = blobMarkdown?.match(/\(([^)]+)\)/);
  13. const blobId = match ? match[1] : blobMarkdown;
  14. const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean) : [];
  15. const content = {
  16. type: 'video',
  17. url: blobId,
  18. createdAt: new Date().toISOString(),
  19. author: userId,
  20. tags,
  21. title: title || '',
  22. description: description || '',
  23. opinions: {},
  24. opinions_inhabitants: []
  25. };
  26. return new Promise((resolve, reject) => {
  27. ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res));
  28. });
  29. },
  30. async updateVideoById(id, blobMarkdown, tagsRaw, title, description) {
  31. const ssbClient = await openSsb();
  32. const userId = ssbClient.id;
  33. return new Promise((resolve, reject) => {
  34. ssbClient.get(id, (err, oldMsg) => {
  35. if (err || !oldMsg || oldMsg.content?.type !== 'video') return reject(new Error('Video not found'));
  36. if (Object.keys(oldMsg.content.opinions || {}).length > 0) return reject(new Error('Cannot edit video after it has received opinions.'));
  37. if (oldMsg.content.author !== userId) return reject(new Error('Not the author'));
  38. const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(Boolean) : oldMsg.content.tags;
  39. const match = blobMarkdown?.match(/\(([^)]+)\)/);
  40. const blobId = match ? match[1] : blobMarkdown;
  41. const tombstone = {
  42. type: 'tombstone',
  43. target: id,
  44. deletedAt: new Date().toISOString(),
  45. author: userId
  46. };
  47. const updated = {
  48. ...oldMsg.content,
  49. url: blobId || oldMsg.content.url,
  50. tags,
  51. title: title || '',
  52. description: description || '',
  53. updatedAt: new Date().toISOString(),
  54. replaces: id
  55. };
  56. ssbClient.publish(tombstone, err => {
  57. if (err) return reject(err);
  58. ssbClient.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result));
  59. });
  60. });
  61. });
  62. },
  63. async deleteVideoById(id) {
  64. const ssbClient = await openSsb();
  65. const userId = ssbClient.id;
  66. return new Promise((resolve, reject) => {
  67. ssbClient.get(id, (err, msg) => {
  68. if (err || !msg || msg.content?.type !== 'video') return reject(new Error('Video not found'));
  69. if (msg.content.author !== userId) return reject(new Error('Not the author'));
  70. const tombstone = {
  71. type: 'tombstone',
  72. target: id,
  73. deletedAt: new Date().toISOString(),
  74. author: userId
  75. };
  76. ssbClient.publish(tombstone, (err2, res) => err2 ? reject(err2) : resolve(res));
  77. });
  78. });
  79. },
  80. async listAll(filter = 'all') {
  81. const ssbClient = await openSsb();
  82. const userId = ssbClient.id;
  83. const messages = await new Promise((res, rej) => {
  84. pull(
  85. ssbClient.createLogStream(),
  86. pull.collect((err, msgs) => err ? rej(err) : res(msgs))
  87. );
  88. });
  89. const tombstoned = new Set();
  90. const replaces = new Map();
  91. const videos = new Map();
  92. for (const m of messages) {
  93. const k = m.key;
  94. const c = m.value.content;
  95. if (!c) continue;
  96. if (c.type === 'tombstone' && c.target) {
  97. tombstoned.add(c.target);
  98. continue;
  99. }
  100. if (c.type !== 'video') continue;
  101. if (tombstoned.has(k)) continue;
  102. if (c.replaces) replaces.set(c.replaces, k);
  103. videos.set(k, {
  104. key: k,
  105. url: c.url,
  106. createdAt: c.createdAt,
  107. updatedAt: c.updatedAt || null,
  108. tags: c.tags || [],
  109. author: c.author,
  110. title: c.title || '',
  111. description: c.description || '',
  112. opinions: c.opinions || {},
  113. opinions_inhabitants: c.opinions_inhabitants || []
  114. });
  115. }
  116. for (const replaced of replaces.keys()) {
  117. videos.delete(replaced);
  118. }
  119. let out = Array.from(videos.values());
  120. if (filter === 'mine') {
  121. out = out.filter(v => v.author === userId);
  122. } else if (filter === 'recent') {
  123. const now = Date.now();
  124. out = out.filter(v => new Date(v.createdAt).getTime() >= now - 86400000);
  125. } else if (filter === 'top') {
  126. out = out.sort((a, b) => {
  127. const sumA = Object.values(a.opinions).reduce((s, v) => s + v, 0);
  128. const sumB = Object.values(b.opinions).reduce((s, v) => s + v, 0);
  129. return sumB - sumA;
  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. const ssbClient = await openSsb();
  158. const userId = ssbClient.id;
  159. return new Promise((resolve, reject) => {
  160. ssbClient.get(id, (err, msg) => {
  161. if (err || !msg || msg.content?.type !== 'video') return reject(new Error('Video not found'));
  162. if (msg.content.opinions_inhabitants?.includes(userId)) return reject(new Error('Already voted'));
  163. const tombstone = {
  164. type: 'tombstone',
  165. target: id,
  166. deletedAt: new Date().toISOString(),
  167. author: userId
  168. };
  169. const updated = {
  170. ...msg.content,
  171. opinions: {
  172. ...msg.content.opinions,
  173. [category]: (msg.content.opinions?.[category] || 0) + 1
  174. },
  175. opinions_inhabitants: [...(msg.content.opinions_inhabitants || []), userId],
  176. updatedAt: new Date().toISOString(),
  177. replaces: id
  178. };
  179. ssbClient.publish(tombstone, err => {
  180. if (err) return reject(err);
  181. ssbClient.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result));
  182. });
  183. });
  184. });
  185. }
  186. };
  187. };