nodebb-plugin-poll 3.2.9 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -19
- package/languages/en_GB/poll.json +12 -4
- package/languages/fr/poll.json +46 -38
- package/lib/config.js +13 -29
- package/lib/hooks.js +82 -68
- package/lib/nodebb.js +0 -1
- package/lib/poll.js +242 -121
- package/lib/sockets.js +60 -44
- package/lib/vote.js +29 -16
- package/library.js +27 -25
- package/package.json +4 -4
- package/plugin.json +8 -5
- package/public/js/admin.js +3 -32
- package/public/js/poll/creator.js +230 -170
- package/public/js/poll/main.js +64 -28
- package/public/js/poll/view.js +67 -81
- package/public/scss/style.scss +1 -4
- package/schema.md +24 -0
- package/templates/admin/partials/widgets/poll.tpl +2 -2
- package/templates/admin/plugins/poll.tpl +63 -48
- package/templates/poll/creator.tpl +18 -22
- package/templates/poll/manage.tpl +18 -0
- package/templates/poll/option-input.tpl +6 -0
- package/templates/poll/view/details.tpl +7 -6
- package/templates/poll/view.tpl +14 -9
- package/upgrades/poll-4.0-upgrade.js +128 -0
- package/lib/scheduler.js +0 -52
- package/lib/serializer.js +0 -4
- package/public/js/poll/serializer.js +0 -173
package/lib/poll.js
CHANGED
|
@@ -1,111 +1,263 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const db = require.main.require('./src/database');
|
|
4
|
+
const privileges = require.main.require('./src/privileges');
|
|
4
5
|
|
|
5
6
|
const Vote = require('./vote');
|
|
6
|
-
const Scheduler = require('./scheduler');
|
|
7
7
|
|
|
8
8
|
const Poll = exports;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
10
|
+
Poll.add = async function (postData, polls) {
|
|
11
|
+
// postData is what is being saved to db
|
|
12
|
+
// polls is submited by user
|
|
13
|
+
const savePolls = [];
|
|
14
|
+
for (const pollData of polls) {
|
|
15
|
+
// eslint-disable-next-line no-await-in-loop
|
|
16
|
+
const pollId = await db.incrObjectField('global', 'nextPollId');
|
|
17
|
+
|
|
18
|
+
const poll = {
|
|
19
|
+
pollId: pollId,
|
|
20
|
+
title: pollData.title,
|
|
21
|
+
uid: postData.editor || postData.uid,
|
|
22
|
+
pid: postData.pid,
|
|
23
|
+
deleted: 0,
|
|
24
|
+
end: pollData.end || 0,
|
|
25
|
+
maximumVotesPerUser: pollData.maximumVotesPerUser || 1,
|
|
26
|
+
timestamp: postData.edited || postData.timestamp,
|
|
27
|
+
};
|
|
28
|
+
if (Object.hasOwn(pollData, 'allowAnonVoting')) {
|
|
29
|
+
poll.allowAnonVoting = pollData.allowAnonVoting;
|
|
30
|
+
}
|
|
31
|
+
if (Object.hasOwn(pollData, 'disallowVoteUpdate')) {
|
|
32
|
+
poll.disallowVoteUpdate = pollData.disallowVoteUpdate;
|
|
33
|
+
}
|
|
34
|
+
// [{id: '1772931625035', title: 'option 1'} {id: '1772931626635', title: 'option 2'}]
|
|
35
|
+
poll.options = JSON.stringify(pollData.options || []);
|
|
36
|
+
|
|
37
|
+
savePolls.push(poll);
|
|
38
|
+
}
|
|
14
39
|
|
|
15
|
-
|
|
16
|
-
pollId
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
ended: 0,
|
|
22
|
-
timestamp: postData.edited || postData.timestamp,
|
|
23
|
-
version: POLL_DATA_VERSION,
|
|
24
|
-
};
|
|
40
|
+
await Promise.all([
|
|
41
|
+
db.setObjectBulk(savePolls.map(p => [`poll:${p.pollId}`, p])),
|
|
42
|
+
db.sortedSetAdd('polls:createtime', savePolls.map(p => p.timestamp), savePolls.map(p => p.pollId)),
|
|
43
|
+
]);
|
|
44
|
+
return savePolls;
|
|
45
|
+
};
|
|
25
46
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
47
|
+
Poll.deletePolls = async function (pollIds) {
|
|
48
|
+
if (!pollIds.length) return;
|
|
49
|
+
const polls = await db.getObjects(pollIds.map(pollId => `poll:${pollId}`));
|
|
50
|
+
const bulkDelete = [];
|
|
51
|
+
polls.forEach((poll) => {
|
|
52
|
+
if (poll && poll.options) {
|
|
53
|
+
const options = tryParseOptions(poll.options);
|
|
54
|
+
options.forEach((option) => {
|
|
55
|
+
bulkDelete.push(`poll:${poll.pollId}:options:${option.id}:votes`);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
30
59
|
|
|
60
|
+
bulkDelete.push(...pollIds.map(pollId => `poll:${pollId}`));
|
|
61
|
+
bulkDelete.push(...pollIds.map(pollId => `poll:${pollId}:voters`));
|
|
62
|
+
bulkDelete.push(...pollIds.map(pollId => `poll:${pollId}:anon:voters`));
|
|
31
63
|
await Promise.all([
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
NodeBB.db.listAppend('polls', pollId),
|
|
35
|
-
NodeBB.db.setObjectField(`topic:${poll.tid}`, 'pollId', pollId),
|
|
36
|
-
NodeBB.db.setAdd(`poll:${pollId}:options`, pollData.options.map(option => option.id)),
|
|
37
|
-
NodeBB.db.setObjectBulk(pollData.options.map(
|
|
38
|
-
option => ([`poll:${pollId}:options:${option.id}`, option])
|
|
39
|
-
)),
|
|
64
|
+
db.deleteAll(bulkDelete),
|
|
65
|
+
db.sortedSetRemove('polls:createtime', pollIds),
|
|
40
66
|
]);
|
|
67
|
+
};
|
|
41
68
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
Poll.edit = async function (postData, newPolls) {
|
|
70
|
+
// figure out which polls are new, which are edited and which are deleted
|
|
71
|
+
const newPollIds = newPolls.map(p => String(p.pollId));
|
|
72
|
+
const currentPollIds = await Poll.getPollIdsByPid(postData.pid);
|
|
73
|
+
const toAdd = newPolls.filter(p => !currentPollIds.includes(String(p.pollId)));
|
|
74
|
+
const toUpdate = newPolls.filter(p => currentPollIds.includes(String(p.pollId)));
|
|
75
|
+
const toDelete = currentPollIds.filter(pollId => !newPollIds.includes(String(pollId)));
|
|
76
|
+
const currentPolls = await db.getObjects(currentPollIds.map(pollId => `poll:${pollId}`));
|
|
77
|
+
|
|
78
|
+
await Poll.deletePolls(toDelete);
|
|
79
|
+
|
|
80
|
+
const savedPolls = await Poll.add(postData, toAdd);
|
|
81
|
+
toAdd.forEach((poll, index) => {
|
|
82
|
+
// overwrite with correct pollId generated in add function
|
|
83
|
+
// this also updates items in newPolls array with correct pollId so that it can be saved to post object at the end
|
|
84
|
+
poll.pollId = savedPolls[index].pollId;
|
|
85
|
+
});
|
|
46
86
|
|
|
47
|
-
|
|
87
|
+
const bulkUpdateOptions = [];
|
|
88
|
+
const bulkDeleteOptions = [];
|
|
89
|
+
toUpdate.forEach((poll) => {
|
|
90
|
+
if (poll) {
|
|
91
|
+
const currentPoll = currentPolls.find(p => String(p.pollId) === String(poll.pollId));
|
|
92
|
+
if (currentPoll) {
|
|
93
|
+
const currentOptions = tryParseOptions(currentPoll.options);
|
|
94
|
+
const newOptions = poll.options || [];
|
|
95
|
+
currentOptions.forEach((option) => {
|
|
96
|
+
if (!newOptions.find(o => String(o.id) === String(option.id))) {
|
|
97
|
+
bulkDeleteOptions.push(`poll:${poll.pollId}:options:${option.id}:votes`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
bulkUpdateOptions.push([`poll:${poll.pollId}`, {
|
|
103
|
+
title: poll.title,
|
|
104
|
+
end: poll.end || 0,
|
|
105
|
+
maximumVotesPerUser: poll.maximumVotesPerUser || 1,
|
|
106
|
+
disallowVoteUpdate: poll.disallowVoteUpdate || 0,
|
|
107
|
+
allowAnonVoting: poll.allowAnonVoting || 0,
|
|
108
|
+
options: JSON.stringify(poll.options || []),
|
|
109
|
+
}]);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
await db.setObjectBulk(bulkUpdateOptions);
|
|
113
|
+
await db.deleteAll(bulkDeleteOptions);
|
|
114
|
+
|
|
115
|
+
await Promise.all(toUpdate.map(poll => updateVotersFromOptions(poll)));
|
|
116
|
+
|
|
117
|
+
if (newPollIds.length) {
|
|
118
|
+
await db.setObjectField(
|
|
119
|
+
`post:${postData.pid}`,
|
|
120
|
+
'pollIds',
|
|
121
|
+
JSON.stringify(newPolls.map(p => p.pollId))
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
await db.deleteObjectField(`post:${postData.pid}`, 'pollIds');
|
|
125
|
+
}
|
|
48
126
|
};
|
|
49
127
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
128
|
+
async function updateVotersFromOptions(poll) {
|
|
129
|
+
// this function updates `poll:${pollId}:voters` sorted set based on the votes on options,
|
|
130
|
+
// this is needed when options are removed from a poll via edit
|
|
131
|
+
const options = poll.options || [];
|
|
132
|
+
|
|
133
|
+
const currentVoters = await db.getSortedSetsMembers(
|
|
134
|
+
options.map(option => `poll:${poll.pollId}:options:${option.id}:votes`)
|
|
135
|
+
);
|
|
136
|
+
const currentVoterSet = [...new Set(currentVoters.flat())];
|
|
137
|
+
const timestamps = await db.sortedSetScores(`poll:${poll.pollId}:voters`, currentVoterSet);
|
|
138
|
+
await db.delete(`poll:${poll.pollId}:voters`);
|
|
139
|
+
await db.sortedSetAdd(`poll:${poll.pollId}:voters`, timestamps, currentVoterSet);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
Poll.get = async function (pollId, uid, withVotes = false) {
|
|
143
|
+
const [info, hasVoted, vote] = await Promise.all([
|
|
144
|
+
Poll.getInfo(pollId, withVotes),
|
|
55
145
|
uid ? Vote.hasUidVoted(uid, pollId) : false,
|
|
56
146
|
uid ? Vote.getUidVote(uid, pollId) : null,
|
|
57
147
|
]);
|
|
58
|
-
|
|
59
|
-
|
|
148
|
+
if (!info) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const canRead = await privileges.posts.can('topics:read', info.pid, uid);
|
|
152
|
+
if (!canRead) {
|
|
153
|
+
throw new Error('[[error:no-privileges]]');
|
|
154
|
+
}
|
|
155
|
+
info.options.forEach((option) => {
|
|
60
156
|
const percentage = ((option.voteCount / info.voteCount) * 100).toFixed(0);
|
|
61
157
|
option.percentage = isNaN(percentage) ? 0 : percentage;
|
|
62
158
|
});
|
|
63
159
|
|
|
64
|
-
|
|
160
|
+
info.disallowVoteUpdate = parseInt(info.disallowVoteUpdate, 10);
|
|
65
161
|
return {
|
|
66
162
|
info,
|
|
67
|
-
options,
|
|
68
|
-
settings,
|
|
163
|
+
options: info.options,
|
|
69
164
|
hasVoted,
|
|
70
165
|
vote,
|
|
71
166
|
};
|
|
72
167
|
};
|
|
73
168
|
|
|
74
|
-
Poll.
|
|
75
|
-
|
|
169
|
+
Poll.getPollIdsByPid = async function (pid) {
|
|
170
|
+
const pollIdsJson = await db.getObjectField(`post:${pid}`, 'pollIds');
|
|
171
|
+
if (!pollIdsJson) {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const pollIds = JSON.parse(pollIdsJson || '[]');
|
|
176
|
+
return Array.isArray(pollIds) ? pollIds.map(p => String(p)) : [];
|
|
177
|
+
} catch (err) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
76
180
|
};
|
|
77
181
|
|
|
78
|
-
Poll.
|
|
79
|
-
|
|
182
|
+
Poll.getPollIdsByPids = async function (pids) {
|
|
183
|
+
const postData = await db.getObjectsFields(pids.map(pid => `post:${pid}`), ['pollIds']);
|
|
184
|
+
return postData.map((post) => {
|
|
185
|
+
if (!post || !post.pollIds) {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const pollIds = JSON.parse(post.pollIds || '[]');
|
|
190
|
+
return Array.isArray(pollIds) ? pollIds.map(p => String(p)) : [];
|
|
191
|
+
} catch (err) {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
});
|
|
80
195
|
};
|
|
81
196
|
|
|
82
|
-
Poll.getInfo = async function (pollId) {
|
|
197
|
+
Poll.getInfo = async function (pollId, withVotes = false) {
|
|
83
198
|
const [poll, voteCount] = await Promise.all([
|
|
84
|
-
|
|
199
|
+
db.getObject(`poll:${pollId}`),
|
|
85
200
|
Poll.getVoteCount(pollId),
|
|
86
201
|
]);
|
|
202
|
+
if (!poll) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
87
205
|
poll.voteCount = parseInt(voteCount, 10) || 0;
|
|
206
|
+
const end = parseInt(poll.end, 10);
|
|
207
|
+
poll.ended = end > 0 && Date.now() > end;
|
|
208
|
+
poll.options = await loadOptions(pollId, poll.options, withVotes);
|
|
88
209
|
return poll;
|
|
89
210
|
};
|
|
90
211
|
|
|
91
212
|
Poll.getOptions = async function (pollId, withVotes = false) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
213
|
+
const optionsJson = await db.getObjectField(`poll:${pollId}`, 'options');
|
|
214
|
+
return await loadOptions(pollId, optionsJson, withVotes);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
Poll.getPollOptionIds = async function (pollId) {
|
|
218
|
+
const optionsJson = await db.getObjectField(`poll:${pollId}`, 'options');
|
|
219
|
+
return tryParseOptions(optionsJson).map(opts => opts && String(opts.id));
|
|
99
220
|
};
|
|
100
221
|
|
|
222
|
+
async function loadOptions(pollId, optionsJson, withVotes = false) {
|
|
223
|
+
const options = tryParseOptions(optionsJson);
|
|
224
|
+
|
|
225
|
+
const votes = await db.getSortedSetsMembers(options.map(o => `poll:${pollId}:options:${o.id}:votes`));
|
|
226
|
+
|
|
227
|
+
options.forEach((option, index) => {
|
|
228
|
+
if (option) {
|
|
229
|
+
option.voteCount = votes[index].length;
|
|
230
|
+
if (withVotes) {
|
|
231
|
+
option.votes = votes[index];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
return options;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function tryParseOptions(optionsJson) {
|
|
239
|
+
if (!optionsJson) return [];
|
|
240
|
+
try {
|
|
241
|
+
const optionsArray = JSON.parse(optionsJson || '[]');
|
|
242
|
+
return Array.isArray(optionsArray) ? optionsArray : [];
|
|
243
|
+
} catch (err) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
101
248
|
Poll.getOption = async function (pollId, option, withVotes = false) {
|
|
102
|
-
const [
|
|
103
|
-
|
|
249
|
+
const [optionsJson, votes, voteCount] = await Promise.all([
|
|
250
|
+
db.getObjectField(`poll:${pollId}`, 'options'),
|
|
104
251
|
withVotes ?
|
|
105
|
-
|
|
252
|
+
db.getSortedSetRange(`poll:${pollId}:options:${option}:votes`, 0, -1) :
|
|
106
253
|
null,
|
|
107
254
|
Poll.getOptionVoteCount(pollId, option),
|
|
108
255
|
]);
|
|
256
|
+
const options = tryParseOptions(optionsJson);
|
|
257
|
+
const optionData = options.find(opt => String(opt.id) === String(option));
|
|
258
|
+
if (!optionData) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
109
261
|
|
|
110
262
|
if (votes) {
|
|
111
263
|
optionData.votes = votes;
|
|
@@ -115,30 +267,28 @@ Poll.getOption = async function (pollId, option, withVotes = false) {
|
|
|
115
267
|
};
|
|
116
268
|
|
|
117
269
|
Poll.getVotersCount = async function (pollId) {
|
|
118
|
-
return await
|
|
270
|
+
return await db.sortedSetCard(`poll:${pollId}:voters`);
|
|
119
271
|
};
|
|
120
272
|
|
|
121
273
|
Poll.getVoteCount = async function (pollId) {
|
|
122
|
-
const
|
|
123
|
-
return await
|
|
124
|
-
|
|
274
|
+
const optionIds = await Poll.getPollOptionIds(pollId);
|
|
275
|
+
return await db.sortedSetsCardSum(
|
|
276
|
+
optionIds.map(option => `poll:${pollId}:options:${option}:votes`)
|
|
125
277
|
);
|
|
126
278
|
};
|
|
127
279
|
|
|
128
280
|
Poll.getOptionVoteCount = async function (pollId, option) {
|
|
129
|
-
return await
|
|
281
|
+
return await db.sortedSetCard(`poll:${pollId}:options:${option}:votes`);
|
|
130
282
|
};
|
|
131
283
|
|
|
132
284
|
Poll.hasOption = async function (pollId, option) {
|
|
133
|
-
|
|
285
|
+
const optionIds = new Set(await Poll.getPollOptionIds(pollId));
|
|
286
|
+
return optionIds.has(String(option));
|
|
134
287
|
};
|
|
135
288
|
|
|
136
289
|
Poll.hasOptions = async function (pollId, options) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Poll.getSettings = async function (pollId) {
|
|
141
|
-
return await NodeBB.db.getObject(`poll:${pollId}:settings`);
|
|
290
|
+
const optionIds = new Set(await Poll.getPollOptionIds(pollId));
|
|
291
|
+
return options.map(option => optionIds.has(String(option)));
|
|
142
292
|
};
|
|
143
293
|
|
|
144
294
|
Poll.isDeleted = async function (pollId) {
|
|
@@ -146,81 +296,52 @@ Poll.isDeleted = async function (pollId) {
|
|
|
146
296
|
return parseInt(result, 10) === 1;
|
|
147
297
|
};
|
|
148
298
|
|
|
149
|
-
Poll.delete = async function (
|
|
150
|
-
await
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await Poll.setFields(pollId, {
|
|
155
|
-
edited: 0,
|
|
156
|
-
deleted: 0,
|
|
157
|
-
});
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
Poll.schedule = async function (pollId) {
|
|
161
|
-
await NodeBB.db.setAdd('polls:scheduled', pollId);
|
|
162
|
-
Scheduler.add(pollId);
|
|
299
|
+
Poll.delete = async function (pollIds) {
|
|
300
|
+
await db.setObject(
|
|
301
|
+
pollIds.map(pollId => `poll:${pollId}`),
|
|
302
|
+
{ deleted: 1 },
|
|
303
|
+
);
|
|
163
304
|
};
|
|
164
305
|
|
|
165
|
-
Poll.
|
|
166
|
-
|
|
306
|
+
Poll.restore = async function (pollIds) {
|
|
307
|
+
await db.setObject(
|
|
308
|
+
pollIds.map(pollId => `poll:${pollId}`),
|
|
309
|
+
{
|
|
310
|
+
edited: 0,
|
|
311
|
+
deleted: 0,
|
|
312
|
+
},
|
|
313
|
+
);
|
|
167
314
|
};
|
|
168
315
|
|
|
169
316
|
Poll.hasEnded = async function (pollId) {
|
|
170
|
-
|
|
171
|
-
|
|
317
|
+
let end = await Poll.getField(pollId, 'end');
|
|
318
|
+
end = parseInt(end, 10);
|
|
319
|
+
return end > 0 && Date.now() > end;
|
|
172
320
|
};
|
|
173
321
|
|
|
174
322
|
Poll.doesDisallowVoteUpdate = async function (pollId) {
|
|
175
|
-
const result = await Poll.
|
|
323
|
+
const result = await Poll.getField(pollId, 'disallowVoteUpdate');
|
|
176
324
|
return parseInt(result, 10) === 1;
|
|
177
325
|
};
|
|
178
326
|
|
|
179
327
|
Poll.doesAllowVoteUpdate = async function (pollId) {
|
|
180
|
-
const result = await Poll.
|
|
328
|
+
const result = await Poll.getField(pollId, 'disallowVoteUpdate');
|
|
181
329
|
return parseInt(result, 10) !== 1;
|
|
182
330
|
};
|
|
183
331
|
|
|
184
|
-
Poll.end = async function (pollId) {
|
|
185
|
-
await NodeBB.db.setRemove('polls:scheduled', pollId);
|
|
186
|
-
await Poll.setField(pollId, 'ended', 1);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
Poll.changePid = async function (pollId, pid) {
|
|
190
|
-
await Promise.all([
|
|
191
|
-
Poll.setField(pollId, 'pid', pid),
|
|
192
|
-
NodeBB.db.setObjectField(`post:${pid}`, 'pollId', pollId),
|
|
193
|
-
]);
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
Poll.changeTid = async function (pollId, tid) {
|
|
197
|
-
await Promise.all([
|
|
198
|
-
Poll.setField(pollId, 'tid', tid),
|
|
199
|
-
NodeBB.db.setObjectField(`topic:${tid}`, 'pollId', pollId),
|
|
200
|
-
]);
|
|
201
|
-
};
|
|
202
|
-
|
|
203
332
|
Poll.setField = async function (pollId, field, value) {
|
|
204
|
-
await
|
|
333
|
+
await db.setObjectField(`poll:${pollId}`, field, value);
|
|
205
334
|
};
|
|
206
335
|
|
|
207
336
|
Poll.setFields = async function (pollId, data) {
|
|
208
|
-
await
|
|
337
|
+
await db.setObject(`poll:${pollId}`, data);
|
|
209
338
|
};
|
|
210
339
|
|
|
211
340
|
Poll.getField = async function (pollId, field) {
|
|
212
|
-
return await
|
|
341
|
+
return await db.getObjectField(`poll:${pollId}`, field);
|
|
213
342
|
};
|
|
214
343
|
|
|
215
344
|
Poll.getFields = async function (pollId, fields) {
|
|
216
|
-
return await
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
Poll.getSettingsField = async function (pollId, field) {
|
|
220
|
-
return await NodeBB.db.getObjectField(`poll:${pollId}:settings`, field);
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
Poll.setSettingsField = async function (pollId, field, value) {
|
|
224
|
-
await NodeBB.db.setObjectField(`poll:${pollId}:settings`, field, value);
|
|
345
|
+
return await db.getObjectFields(`poll:${pollId}`, fields);
|
|
225
346
|
};
|
|
226
347
|
|
package/lib/sockets.js
CHANGED
|
@@ -1,43 +1,44 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const _ = require.main.require('lodash');
|
|
4
|
-
const
|
|
4
|
+
const db = require.main.require('./src/database');
|
|
5
|
+
const posts = require.main.require('./src/posts');
|
|
6
|
+
const topics = require.main.require('./src/topics');
|
|
7
|
+
const privileges = require.main.require('./src/privileges');
|
|
5
8
|
|
|
9
|
+
const NodeBB = require('./nodebb');
|
|
6
10
|
const Config = require('./config');
|
|
7
11
|
const Poll = require('./poll');
|
|
8
12
|
const Vote = require('./vote');
|
|
9
13
|
|
|
10
14
|
const Sockets = exports;
|
|
11
15
|
|
|
12
|
-
Sockets.getConfig = function (socket, data, callback) {
|
|
13
|
-
callback(null, Config.settings.get());
|
|
14
|
-
};
|
|
15
|
-
|
|
16
16
|
Sockets.get = async function (socket, data) {
|
|
17
|
-
const
|
|
17
|
+
const settings = await Config.getSettings();
|
|
18
|
+
const allowAnon = parseInt(settings.allowGuestsToViewResults, 10) === 1;
|
|
18
19
|
|
|
19
20
|
if (!data) {
|
|
20
21
|
throw new Error('Invalid request, request data is not defined');
|
|
21
22
|
}
|
|
22
|
-
if (
|
|
23
|
+
if (!Array.isArray(data.pollIds) || !data.pollIds.length) {
|
|
23
24
|
throw new Error('Invalid request, pollId is required');
|
|
24
25
|
}
|
|
25
26
|
if ((!socket.uid && !allowAnon)) {
|
|
26
27
|
throw new Error('Invalid request, anonymous access is not allowed');
|
|
27
28
|
}
|
|
29
|
+
return await Promise.all(data.pollIds.map(async (pollId) => {
|
|
30
|
+
const pollData = await Poll.get(pollId, socket.uid, !!socket.uid);
|
|
31
|
+
if (!pollData) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
throw new Error('Legacy polls are not supported');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (parseInt(pollData.settings.allowAnonVoting, 10) === 1) {
|
|
36
|
-
await anonymizeVoters(socket.uid, data.pollId, pollData.options);
|
|
37
|
-
}
|
|
35
|
+
if (parseInt(pollData.allowAnonVoting, 10) === 1) {
|
|
36
|
+
await anonymizeVoters(socket.uid, pollId, pollData.options);
|
|
37
|
+
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
pollData.optionType = parseInt(pollData.maximumVotesPerUser, 10) > 1 ? 'checkbox' : 'radio';
|
|
40
|
+
return pollData;
|
|
41
|
+
}));
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
Sockets.vote = async function (socket, data) {
|
|
@@ -50,21 +51,21 @@ Sockets.vote = async function (socket, data) {
|
|
|
50
51
|
|
|
51
52
|
data.uid = socket.uid;
|
|
52
53
|
|
|
53
|
-
const [canVote, optionsFilter
|
|
54
|
+
const [pollData, canVote, optionsFilter] = await Promise.all([
|
|
55
|
+
db.getObject(`poll:${data.pollId}`),
|
|
54
56
|
Vote.canVote(data.uid, data.pollId),
|
|
55
57
|
Poll.hasOptions(data.pollId, data.options),
|
|
56
|
-
Poll.getSettings(data.pollId),
|
|
57
58
|
]);
|
|
58
59
|
|
|
59
60
|
// Filter the options on their existence
|
|
60
61
|
data.options = data.options.filter((el, index) => optionsFilter[index]);
|
|
61
62
|
|
|
62
63
|
// Give an error if there are too many votes
|
|
63
|
-
if (data.options.length > parseInt(
|
|
64
|
-
throw new Error(`You can only vote for ${
|
|
64
|
+
if (data.options.length > parseInt(pollData.maximumVotesPerUser, 10)) {
|
|
65
|
+
throw new Error(`You can only vote for ${pollData.maximumVotesPerUser} options on this poll.`);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
if (data.voteAnon && parseInt(
|
|
68
|
+
if (data.voteAnon && parseInt(pollData.allowAnonVoting, 10) !== 1) {
|
|
68
69
|
throw new Error('[[poll:error.anon-voting-not-allowed]]');
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -74,8 +75,11 @@ Sockets.vote = async function (socket, data) {
|
|
|
74
75
|
|
|
75
76
|
await Vote.add(data);
|
|
76
77
|
|
|
77
|
-
const
|
|
78
|
-
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
|
|
78
|
+
const returnPoll = await Poll.get(data.pollId, socket.uid, false);
|
|
79
|
+
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
|
|
80
|
+
pollData: returnPoll,
|
|
81
|
+
uid: socket.uid,
|
|
82
|
+
});
|
|
79
83
|
};
|
|
80
84
|
|
|
81
85
|
Sockets.updateVote = async function (socket, data) {
|
|
@@ -87,18 +91,18 @@ Sockets.updateVote = async function (socket, data) {
|
|
|
87
91
|
}
|
|
88
92
|
data.uid = socket.uid;
|
|
89
93
|
|
|
90
|
-
const [canUpdateVote, optionsFilter
|
|
94
|
+
const [pollData, canUpdateVote, optionsFilter] = await Promise.all([
|
|
95
|
+
db.getObject(`poll:${data.pollId}`),
|
|
91
96
|
Vote.canUpdateVote(data.uid, data.pollId),
|
|
92
97
|
Poll.hasOptions(data.pollId, data.options),
|
|
93
|
-
Poll.getSettings(data.pollId),
|
|
94
98
|
]);
|
|
95
99
|
|
|
96
100
|
// Filter the options on their existence
|
|
97
101
|
data.options = data.options.filter((el, index) => optionsFilter[index]);
|
|
98
102
|
|
|
99
103
|
// Give an error if there are too many votes
|
|
100
|
-
if (data.options.length > parseInt(
|
|
101
|
-
throw new Error(`You can only vote for ${
|
|
104
|
+
if (data.options.length > parseInt(pollData.maximumVotesPerUser, 10)) {
|
|
105
|
+
throw new Error(`You can only vote for ${pollData.maximumVotesPerUser} options on this poll.`);
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
if (!canUpdateVote) {
|
|
@@ -110,8 +114,11 @@ Sockets.updateVote = async function (socket, data) {
|
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
await Vote.update(data);
|
|
113
|
-
const
|
|
114
|
-
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
|
|
117
|
+
const returnPoll = await Poll.get(data.pollId, socket.uid, false);
|
|
118
|
+
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
|
|
119
|
+
pollData: returnPoll,
|
|
120
|
+
uid: socket.uid,
|
|
121
|
+
});
|
|
115
122
|
};
|
|
116
123
|
|
|
117
124
|
Sockets.removeVote = async function (socket, data) {
|
|
@@ -130,24 +137,32 @@ Sockets.removeVote = async function (socket, data) {
|
|
|
130
137
|
|
|
131
138
|
await Vote.remove(data);
|
|
132
139
|
const pollData = await Poll.get(data.pollId, socket.uid, false);
|
|
133
|
-
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
|
|
140
|
+
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
|
|
141
|
+
pollData,
|
|
142
|
+
uid: socket.uid,
|
|
143
|
+
});
|
|
134
144
|
};
|
|
135
145
|
|
|
136
146
|
Sockets.getOptionDetails = async function (socket, data) {
|
|
137
147
|
if (!socket.uid || !data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) {
|
|
138
148
|
throw new Error('Invalid request');
|
|
139
149
|
}
|
|
140
|
-
const [poll,
|
|
150
|
+
const [poll, option] = await Promise.all([
|
|
141
151
|
Poll.getInfo(data.pollId),
|
|
142
|
-
Poll.getSettings(data.pollId),
|
|
143
152
|
Poll.getOption(data.pollId, data.optionId, true),
|
|
144
153
|
]);
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
if (!poll) {
|
|
155
|
+
throw new Error('[[error:invalid-data]]');
|
|
156
|
+
}
|
|
157
|
+
const canRead = await privileges.posts.can('topics:read', poll.pid, socket.uid);
|
|
158
|
+
if (!canRead) {
|
|
159
|
+
throw new Error('[[error:no-privileges]]');
|
|
160
|
+
}
|
|
161
|
+
if (!option || !option.votes || !option.votes.length) {
|
|
147
162
|
return option;
|
|
148
163
|
}
|
|
149
164
|
|
|
150
|
-
if (parseInt(
|
|
165
|
+
if (parseInt(poll.allowAnonVoting, 10) === 1) {
|
|
151
166
|
await anonymizeVoters(socket.uid, data.pollId, [option]);
|
|
152
167
|
}
|
|
153
168
|
|
|
@@ -164,21 +179,22 @@ Sockets.getOptionDetails = async function (socket, data) {
|
|
|
164
179
|
};
|
|
165
180
|
|
|
166
181
|
Sockets.canCreate = async function (socket, data) {
|
|
167
|
-
if (!socket.uid || !data
|
|
182
|
+
if (!socket.uid || !data) {
|
|
168
183
|
throw new Error('Invalid request');
|
|
169
184
|
}
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
let { cid, tid, pid } = data;
|
|
186
|
+
if (pid) {
|
|
187
|
+
cid = await posts.getCidByPid(pid);
|
|
188
|
+
} else if (tid) {
|
|
189
|
+
cid = await topics.getTopicField(tid, 'cid');
|
|
174
190
|
}
|
|
175
|
-
return await checkPrivs(
|
|
191
|
+
return await checkPrivs(cid, socket.uid);
|
|
176
192
|
};
|
|
177
193
|
|
|
178
194
|
async function anonymizeVoters(callerUid, pollId, options) {
|
|
179
195
|
const uids = _.uniq(options.map(opt => opt.votes).flat());
|
|
180
196
|
const [isAnon, isPrivileged] = await Promise.all([
|
|
181
|
-
|
|
197
|
+
db.isSortedSetMembers(`poll:${pollId}:anon:voters`, uids),
|
|
182
198
|
NodeBB.User.isPrivileged(callerUid),
|
|
183
199
|
]);
|
|
184
200
|
if (isPrivileged) {
|