| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- const pull = require('../server/node_modules/pull-stream');
- const moment = require('../server/node_modules/moment');
- const { getConfig } = require('../configs/config-manager.js');
- const logLimit = getConfig().ssbLogStream?.limit || 1000;
- module.exports = ({ cooler }) => {
- let ssb;
- const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
- return {
- async createVote(question, deadline, options = ['YES', 'NO', 'ABSTENTION', 'CONFUSED', 'FOLLOW_MAJORITY', 'NOT_INTERESTED'], tagsRaw = []) {
- const ssb = await openSsb();
- const userId = ssb.id;
- const parsedDeadline = moment(deadline, moment.ISO_8601, true);
- if (!parsedDeadline.isValid() || parsedDeadline.isBefore(moment())) throw new Error('Invalid deadline');
- const tags = Array.isArray(tagsRaw) ? tagsRaw.filter(Boolean) : tagsRaw.split(',').map(t => t.trim()).filter(Boolean);
- const content = {
- type: 'votes',
- question,
- options,
- deadline: parsedDeadline.toISOString(),
- createdBy: userId,
- status: 'OPEN',
- votes: options.reduce((acc, opt) => ({ ...acc, [opt]: 0 }), {}),
- totalVotes: 0,
- voters: [],
- tags,
- opinions: {},
- opinions_inhabitants: [],
- createdAt: new Date().toISOString()
- };
- return new Promise((res, rej) => ssb.publish(content, (err, msg) => err ? rej(err) : res(msg)));
- },
- async deleteVoteById(id) {
- const ssb = await openSsb();
- const userId = ssb.id;
- const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
- if (vote.content.createdBy !== userId) throw new Error('Not the author');
- const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
- return new Promise((res, rej) => ssb.publish(tombstone, (err, result) => err ? rej(err) : res(result)));
- },
-
- async updateVoteById(id, { question, deadline, options, tags }) {
- const ssb = await openSsb();
- const userId = ssb.id;
- const oldMsg = await new Promise((res, rej) =>
- ssb.get(id, (err, msg) => err || !msg ? rej(new Error('Vote not found')) : res(msg))
- );
- const c = oldMsg.content;
- if (c.type !== 'votes') throw new Error('Invalid type');
- if (c.createdBy !== userId) throw new Error('Not the author');
- let newDeadline = c.deadline;
- if (deadline != null && deadline !== '') {
- const parsed = moment(deadline, moment.ISO_8601, true);
- if (!parsed.isValid() || parsed.isBefore(moment())) throw new Error('Invalid deadline');
- newDeadline = parsed.toISOString();
- }
- let newOptions = c.options;
- let newVotesMap = c.votes;
- let newTotalVotes = c.totalVotes;
- const optionsCambiaron = Array.isArray(options) && (
- options.length !== c.options.length ||
- options.some((o, i) => o !== c.options[i])
- );
- if (optionsCambiaron) {
- if (c.totalVotes > 0) {
- throw new Error('Cannot change options after voting has started');
- }
- newOptions = options;
- newVotesMap = newOptions.reduce((acc, opt) => (acc[opt] = 0, acc), {});
- newTotalVotes = 0;
- }
- const newTags =
- Array.isArray(tags) ? tags.filter(Boolean)
- : typeof tags === 'string' ? tags.split(',').map(t => t.trim()).filter(Boolean)
- : c.tags || [];
- const updated = {
- ...c,
- replaces: id,
- question: question ?? c.question,
- deadline: newDeadline,
- options: newOptions,
- votes: newVotesMap,
- totalVotes: newTotalVotes,
- tags: newTags,
- updatedAt: new Date().toISOString()
- };
- return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
- },
- async voteOnVote(id, choice) {
- const ssb = await openSsb();
- const userId = ssb.id;
- const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
- if (!vote.content.options.includes(choice)) throw new Error('Invalid choice');
- if (vote.content.voters.includes(userId)) throw new Error('Already voted');
- vote.content.votes[choice] += 1;
- vote.content.voters.push(userId);
- vote.content.totalVotes += 1;
- const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
- const updated = { ...vote.content, updatedAt: new Date().toISOString(), replaces: id };
- await new Promise((res, rej) => ssb.publish(tombstone, err => err ? rej(err) : res()));
- return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
- },
- async getVoteById(id) {
- const ssb = await openSsb();
- const now = moment();
- const results = await new Promise((resolve, reject) => {
- pull(
- ssb.createLogStream({ limit: logLimit }),
- pull.collect((err, arr) => err ? reject(err) : resolve(arr))
- );
- });
- const votesByKey = new Map();
- const latestByRoot = new Map();
- for (const r of results) {
- const key = r.key;
- const v = r.value;
- const c = v && v.content;
- if (!c) continue;
- if (c.type === 'votes') {
- votesByKey.set(key, c);
- const ts = Number(v.timestamp || r.timestamp || Date.now());
- const root = c.replaces || key;
- const prev = latestByRoot.get(root);
- if (!prev || ts > prev.ts) latestByRoot.set(root, { key, ts });
- }
- }
- const latestEntry = latestByRoot.get(id);
- let latestId = latestEntry ? latestEntry.key : id;
- let content = votesByKey.get(latestId);
- if (!content) {
- const orig = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
- content = orig.content;
- latestId = id;
- }
- const status = content.status === 'OPEN' && moment(content.deadline).isBefore(now) ? 'CLOSED' : content.status;
- return { id, latestId, ...content, status };
- },
- async listAll(filter = 'all') {
- const ssb = await openSsb();
- const userId = ssb.id;
- const now = moment();
- return new Promise((resolve, reject) => {
- pull(ssb.createLogStream({ limit: logLimit }),
- pull.collect((err, results) => {
- if (err) return reject(err);
- const tombstoned = new Set();
- const replaced = new Map();
- const votes = new Map();
- for (const r of results) {
- const { key, value: { content: c } } = r;
- if (!c) continue;
- if (c.type === 'tombstone') tombstoned.add(c.target);
- if (c.type === 'votes') {
- if (c.replaces) replaced.set(c.replaces, key);
- const status = c.status === 'OPEN' && moment(c.deadline).isBefore(now) ? 'CLOSED' : c.status;
- votes.set(key, { id: key, ...c, status });
- }
- }
- tombstoned.forEach(id => votes.delete(id));
- replaced.forEach((_, oldId) => votes.delete(oldId));
- const out = [...votes.values()];
- if (filter === 'mine') return resolve(out.filter(v => v.createdBy === userId));
- if (filter === 'open') return resolve(out.filter(v => v.status === 'OPEN'));
- if (filter === 'closed') return resolve(out.filter(v => v.status === 'CLOSED'));
- resolve(out);
- }));
- });
- },
- async createOpinion(id, category) {
- const ssb = await openSsb();
- const userId = ssb.id;
- const vote = await new Promise((res, rej) => ssb.get(id, (err, vote) => err ? rej(new Error('Vote not found')) : res(vote)));
- if (vote.content.opinions_inhabitants.includes(userId)) throw new Error('Already voted');
- const tombstone = { type: 'tombstone', target: id, deletedAt: new Date().toISOString(), author: userId };
- const updated = {
- ...vote.content,
- opinions: { ...vote.content.opinions, [category]: (vote.content.opinions[category] || 0) + 1 },
- opinions_inhabitants: [...vote.content.opinions_inhabitants, userId],
- updatedAt: new Date().toISOString(),
- replaces: id
- };
- await new Promise((res, rej) => ssb.publish(tombstone, err => err ? rej(err) : res()));
- return new Promise((res, rej) => ssb.publish(updated, (err, result) => err ? rej(err) : res(result)));
- }
- };
- };
|