events_model.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. const pull = require('../server/node_modules/pull-stream');
  2. const moment = require('../server/node_modules/moment');
  3. const { config } = require('../server/SSB_server.js');
  4. const userId = config.keys.id;
  5. module.exports = ({ cooler }) => {
  6. let ssb;
  7. const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
  8. return {
  9. type: 'event',
  10. async createEvent(title, description, date, location, price = 0, url = "", attendees = [], tagsRaw = [], isPublic) {
  11. const ssbClient = await openSsb();
  12. const formattedDate = date ? moment(date, moment.ISO_8601, true).toISOString() : moment().toISOString();
  13. if (!moment(formattedDate, moment.ISO_8601, true).isValid()) throw new Error("Invalid date format");
  14. if (moment(formattedDate).isBefore(moment(), 'minute')) throw new Error("Cannot create an event in the past");
  15. if (!Array.isArray(attendees)) attendees = attendees.split(',').map(s => s.trim()).filter(Boolean);
  16. attendees.push(userId);
  17. const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) : tagsRaw.split(',').map(s => s.trim()).filter(Boolean);
  18. let p = typeof price === 'string' ? parseFloat(price.replace(',', '.')) : price;
  19. if (isNaN(p)) p = 0;
  20. const content = {
  21. type: 'event',
  22. title,
  23. description,
  24. date: formattedDate,
  25. location,
  26. price: p.toFixed(6),
  27. url,
  28. attendees,
  29. tags,
  30. createdAt: new Date().toISOString(),
  31. organizer: userId,
  32. status: 'OPEN',
  33. opinions: {},
  34. opinions_inhabitants: [],
  35. isPublic
  36. };
  37. return new Promise((resolve, reject) => {
  38. ssbClient.publish(content, (err, res) => err ? reject(err) : resolve(res));
  39. });
  40. },
  41. async toggleAttendee(eventId) {
  42. const ssbClient = await openSsb();
  43. return new Promise((resolve, reject) => {
  44. ssbClient.get(eventId, async (err, ev) => {
  45. if (err || !ev || !ev.content) return reject(new Error("Error retrieving event"));
  46. let attendees = Array.isArray(ev.content.attendees) ? [...ev.content.attendees] : [];
  47. const idx = attendees.indexOf(userId);
  48. if (idx !== -1) attendees.splice(idx, 1); else attendees.push(userId);
  49. const tombstone = {
  50. type: 'tombstone',
  51. target: eventId,
  52. deletedAt: new Date().toISOString(),
  53. author: userId
  54. };
  55. const updated = {
  56. ...ev.content,
  57. attendees,
  58. updatedAt: new Date().toISOString(),
  59. replaces: eventId
  60. };
  61. ssbClient.publish(tombstone, err => {
  62. if (err) return reject(err);
  63. ssbClient.publish(updated, (err2, res) => err2 ? reject(err2) : resolve(res));
  64. });
  65. });
  66. });
  67. },
  68. async deleteEventById(eventId) {
  69. const ssbClient = await openSsb();
  70. return new Promise((resolve, reject) => {
  71. ssbClient.get(eventId, (err, ev) => {
  72. if (err || !ev || !ev.content) return reject(new Error("Error retrieving event"));
  73. if (ev.content.organizer !== userId) return reject(new Error("Only the organizer can delete this event"));
  74. const tombstone = {
  75. type: 'tombstone',
  76. target: eventId,
  77. deletedAt: new Date().toISOString(),
  78. author: userId
  79. };
  80. ssbClient.publish(tombstone, (err, res) => err ? reject(err) : resolve(res));
  81. });
  82. });
  83. },
  84. async listAll(author = null, filter = 'all') {
  85. const ssbClient = await openSsb();
  86. return new Promise((resolve, reject) => {
  87. pull(
  88. ssbClient.createLogStream(),
  89. pull.collect(async (err, results) => {
  90. if (err) return reject(new Error("Error listing events: " + err.message));
  91. const tombstoned = new Set();
  92. const replaces = new Map();
  93. const byId = new Map();
  94. for (const r of results) {
  95. const k = r.key;
  96. const c = r.value.content;
  97. if (!c) continue;
  98. if (c.type === 'tombstone' && c.target) {
  99. tombstoned.add(c.target);
  100. continue;
  101. }
  102. if (c.type === 'event') {
  103. if (tombstoned.has(k)) continue;
  104. if (c.replaces) replaces.set(c.replaces, k);
  105. if (author && c.organizer !== author) continue;
  106. let status = c.status || 'OPEN';
  107. const dateM = moment(c.date);
  108. if (dateM.isValid() && dateM.isBefore(moment()) && status !== 'CLOSED') {
  109. const tombstone = {
  110. type: 'tombstone',
  111. target: k,
  112. deletedAt: new Date().toISOString(),
  113. author: c.organizer
  114. };
  115. const updated = {
  116. ...c,
  117. status: 'CLOSED',
  118. updatedAt: new Date().toISOString(),
  119. replaces: k
  120. };
  121. await new Promise((res, rej) => ssbClient.publish(tombstone, err => err ? rej(err) : res()));
  122. await new Promise((res, rej) => ssbClient.publish(updated, err => err ? rej(err) : res()));
  123. status = 'CLOSED';
  124. }
  125. byId.set(k, {
  126. id: k,
  127. title: c.title,
  128. description: c.description,
  129. date: c.date,
  130. location: c.location,
  131. price: c.price,
  132. url: c.url,
  133. attendees: c.attendees || [],
  134. tags: c.tags || [],
  135. createdAt: c.createdAt,
  136. organizer: c.organizer,
  137. status,
  138. opinions: c.opinions || {},
  139. opinions_inhabitants: c.opinions_inhabitants || [],
  140. isPublic: c.isPublic
  141. });
  142. }
  143. }
  144. for (const replaced of replaces.keys()) {
  145. byId.delete(replaced);
  146. }
  147. let out = Array.from(byId.values());
  148. if (filter === 'mine') out = out.filter(e => e.organizer === userId);
  149. if (['features', 'bugs', 'abuse', 'content'].includes(filter)) out = out.filter(e => e.category === filter);
  150. if (filter === 'confirmed') out = out.filter(e => e.confirmations?.length >= 3);
  151. if (['open', 'resolved', 'invalid', 'underreview'].includes(filter)) out = out.filter(e => e.status.toLowerCase() === filter);
  152. resolve(out);
  153. })
  154. );
  155. });
  156. },
  157. async updateEventById(eventId, updatedData) {
  158. const ssbClient = await openSsb();
  159. return new Promise((resolve, reject) => {
  160. ssbClient.get(eventId, (err, ev) => {
  161. if (err || !ev || !ev.content) return reject(new Error("Error retrieving event"));
  162. if (Object.keys(ev.content.opinions || {}).length > 0) return reject(new Error('Cannot edit event after it has received opinions'));
  163. if (ev.content.organizer !== userId) return reject(new Error("Only the organizer can update this event"));
  164. const tags = updatedData.tags ? updatedData.tags.split(',').map(t => t.trim()).filter(Boolean) : ev.content.tags;
  165. const attendees = updatedData.attendees ? updatedData.attendees.split(',').map(t => t.trim()).filter(Boolean) : ev.content.attendees;
  166. const tombstone = {
  167. type: 'tombstone',
  168. target: eventId,
  169. deletedAt: new Date().toISOString(),
  170. author: userId
  171. };
  172. const updated = {
  173. ...ev.content,
  174. ...updatedData,
  175. attendees,
  176. tags,
  177. updatedAt: new Date().toISOString(),
  178. replaces: eventId
  179. };
  180. ssbClient.publish(tombstone, err => {
  181. if (err) return reject(err);
  182. ssbClient.publish(updated, (err2, res) => err2 ? reject(err2) : resolve(res));
  183. });
  184. });
  185. });
  186. },
  187. async getEventById(eventId) {
  188. const ssbClient = await openSsb();
  189. return new Promise((resolve, reject) => {
  190. ssbClient.get(eventId, async (err, msg) => {
  191. if (err || !msg || !msg.content) return reject(new Error("Error retrieving event"));
  192. const c = msg.content;
  193. const dateM = moment(c.date);
  194. let status = c.status || 'OPEN';
  195. if (dateM.isValid() && dateM.isBefore(moment()) && status !== 'CLOSED') {
  196. const tombstone = {
  197. type: 'tombstone',
  198. target: eventId,
  199. deletedAt: new Date().toISOString(),
  200. author: userId
  201. };
  202. const updated = {
  203. ...c,
  204. status: 'CLOSED',
  205. updatedAt: new Date().toISOString(),
  206. replaces: eventId
  207. };
  208. await ssbClient.publish(tombstone);
  209. await ssbClient.publish(updated);
  210. status = 'CLOSED';
  211. }
  212. resolve({
  213. id: eventId,
  214. title: c.title || '',
  215. description: c.description || '',
  216. date: c.date || '',
  217. location: c.location || '',
  218. price: c.price || 0,
  219. url: c.url || '',
  220. attendees: c.attendees || [],
  221. tags: c.tags || [],
  222. createdAt: c.createdAt || new Date().toISOString(),
  223. updatedAt: c.updatedAt || new Date().toISOString(),
  224. opinions: c.opinions || {},
  225. opinions_inhabitants: c.opinions_inhabitants || [],
  226. organizer: c.organizer || '',
  227. status,
  228. isPublic: c.isPublic || 'private'
  229. });
  230. });
  231. });
  232. },
  233. async createOpinion(id, category) {
  234. const ssbClient = await openSsb();
  235. return new Promise((resolve, reject) => {
  236. ssbClient.get(id, (err, msg) => {
  237. if (err || !msg || msg.content?.type !== 'event') return reject(new Error('Event not found'));
  238. if (msg.content.opinions_inhabitants?.includes(userId)) return reject(new Error('Already voted'));
  239. const tombstone = {
  240. type: 'tombstone',
  241. target: id,
  242. deletedAt: new Date().toISOString(),
  243. author: userId
  244. };
  245. const updated = {
  246. ...msg.content,
  247. opinions: {
  248. ...msg.content.opinions,
  249. [category]: (msg.content.opinions?.[category] || 0) + 1
  250. },
  251. opinions_inhabitants: [...(msg.content.opinions_inhabitants || []), userId],
  252. updatedAt: new Date().toISOString(),
  253. replaces: id
  254. };
  255. ssbClient.publish(tombstone, err => {
  256. if (err) return reject(err);
  257. ssbClient.publish(updated, (err2, result) => err2 ? reject(err2) : resolve(result));
  258. });
  259. });
  260. });
  261. }
  262. };
  263. };