nodebb-plugin-poll 4.0.14 → 4.1.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/.editorconfig +7 -7
- package/LICENSE +20 -20
- package/eslint.config.mjs +7 -0
- package/languages/cs/poll.json +47 -47
- package/languages/de/poll.json +1 -1
- package/languages/en_GB/poll.json +1 -0
- package/languages/en_US/poll.json +52 -52
- package/languages/es/poll.json +39 -39
- package/languages/fr/poll.json +1 -1
- package/languages/gl/poll.json +38 -38
- package/languages/he/poll.json +1 -1
- package/languages/it/poll.json +1 -1
- package/languages/pl/poll.json +1 -1
- package/languages/pt-PT/poll.json +41 -41
- package/languages/ro/poll.json +39 -39
- package/languages/ru/poll.json +1 -1
- package/languages/tr/poll.json +47 -47
- package/lib/config.js +12 -1
- package/lib/hooks.js +231 -14
- package/lib/nodebb.js +0 -0
- package/lib/poll.js +147 -0
- package/lib/sockets.js +117 -12
- package/lib/vote.js +4 -0
- package/library.js +1 -1
- package/package-lock.json +1658 -0
- package/package.json +2 -2
- package/plugin.json +2 -0
- package/public/js/admin.js +0 -0
- package/public/js/poll/creator.js +26 -4
- package/public/js/poll/main.js +19 -8
- package/public/js/poll/view.js +31 -18
- package/templates/admin/plugins/poll.tpl +10 -11
- package/templates/poll/creator.tpl +14 -12
- package/templates/poll/manage.tpl +1 -1
- package/templates/poll/view/messages.tpl +2 -2
- package/templates/poll/view/results.tpl +9 -3
- package/templates/poll/view/voting.tpl +5 -5
- package/templates/poll/view.tpl +8 -8
- package/test/helpers.js +49 -0
- package/test/index.js +530 -0
- package/upgrades/fix_incorrect_poll_privilege.js +15 -15
package/lib/sockets.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const nconf = nodebb.require('nconf');
|
|
3
4
|
const _ = nodebb.require('lodash');
|
|
4
5
|
const db = nodebb.require('./src/database');
|
|
5
6
|
const posts = nodebb.require('./src/posts');
|
|
@@ -15,7 +16,7 @@ const Sockets = exports;
|
|
|
15
16
|
|
|
16
17
|
Sockets.get = async function (socket, data) {
|
|
17
18
|
const settings = await Config.getSettings();
|
|
18
|
-
const allowAnon = settings.allowGuestsToViewResults
|
|
19
|
+
const allowAnon = !!settings.allowGuestsToViewResults;
|
|
19
20
|
|
|
20
21
|
if (!data) {
|
|
21
22
|
throw new Error('Invalid request, request data is not defined');
|
|
@@ -27,13 +28,13 @@ Sockets.get = async function (socket, data) {
|
|
|
27
28
|
return [];
|
|
28
29
|
}
|
|
29
30
|
return await Promise.all(data.pollIds.map(async (pollId) => {
|
|
30
|
-
const withVotes =
|
|
31
|
+
const withVotes = !!socket.uid;
|
|
31
32
|
const pollData = await Poll.get(pollId, socket.uid, withVotes);
|
|
32
33
|
if (!pollData) {
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
if (parseInt(pollData.info.allowAnonVoting, 10) === 1) {
|
|
37
|
+
if (withVotes && parseInt(pollData.info.allowAnonVoting, 10) === 1) {
|
|
37
38
|
await anonymizeVoters(socket.uid, pollId, pollData.options);
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -76,6 +77,13 @@ Sockets.vote = async function (socket, data) {
|
|
|
76
77
|
|
|
77
78
|
await Vote.add(data);
|
|
78
79
|
|
|
80
|
+
// Federate the vote out (FEP-9967) if this is a remote poll
|
|
81
|
+
if (parseInt(pollData.remote, 10) === 1) {
|
|
82
|
+
await federateVoteChange({
|
|
83
|
+
pollData, socket, voteAnon: data.voteAnon, removedOptionIds: [], addedOptionIds: data.options,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
const returnPoll = await Poll.get(data.pollId, socket.uid, false);
|
|
80
88
|
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
|
|
81
89
|
pollData: returnPoll,
|
|
@@ -114,7 +122,20 @@ Sockets.updateVote = async function (socket, data) {
|
|
|
114
122
|
throw new Error('Invalid option');
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
// Capture the prior vote so we can federate the diff (remote polls only)
|
|
126
|
+
const isRemote = parseInt(pollData.remote, 10) === 1;
|
|
127
|
+
const oldOptions = isRemote ? (await Vote.get({ uid: socket.uid, pollId: data.pollId })).options : [];
|
|
128
|
+
|
|
117
129
|
await Vote.update(data);
|
|
130
|
+
|
|
131
|
+
// Federate the vote change out (FEP-9967) if this is a remote poll
|
|
132
|
+
if (isRemote) {
|
|
133
|
+
const oldSet = oldOptions.map(String);
|
|
134
|
+
const removedOptionIds = oldOptions.filter(id => !data.options.map(String).includes(String(id)));
|
|
135
|
+
const addedOptionIds = data.options.filter(id => !oldSet.includes(String(id)));
|
|
136
|
+
await federateVoteChange({ pollData, socket, voteAnon: data.voteAnon, removedOptionIds, addedOptionIds });
|
|
137
|
+
}
|
|
138
|
+
|
|
118
139
|
const returnPoll = await Poll.get(data.pollId, socket.uid, false);
|
|
119
140
|
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
|
|
120
141
|
pollData: returnPoll,
|
|
@@ -136,29 +157,39 @@ Sockets.removeVote = async function (socket, data) {
|
|
|
136
157
|
throw new Error('Can\'t remove vote');
|
|
137
158
|
}
|
|
138
159
|
|
|
160
|
+
const pollData = await db.getObject(`poll:${data.pollId}`);
|
|
161
|
+
const isRemote = parseInt(pollData.remote, 10) === 1;
|
|
162
|
+
// Capture the prior vote + anon flag before removal (remote polls only)
|
|
163
|
+
const oldOptions = isRemote ? (await Vote.get({ uid: socket.uid, pollId: data.pollId })).options : [];
|
|
164
|
+
const voteAnon = isRemote ? await db.isSortedSetMember(`poll:${data.pollId}:anon:voters`, socket.uid) : false;
|
|
165
|
+
|
|
139
166
|
await Vote.remove(data);
|
|
140
|
-
|
|
167
|
+
|
|
168
|
+
// Federate the vote removal out (FEP-9967) if this is a remote poll
|
|
169
|
+
if (isRemote) {
|
|
170
|
+
await federateVoteChange({ pollData, socket, voteAnon, removedOptionIds: oldOptions, addedOptionIds: [] });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const returnPoll = await Poll.get(data.pollId, socket.uid, false);
|
|
141
174
|
NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
|
|
142
|
-
pollData,
|
|
175
|
+
pollData: returnPoll,
|
|
143
176
|
uid: socket.uid,
|
|
144
177
|
});
|
|
145
178
|
};
|
|
146
179
|
|
|
147
180
|
Sockets.getOptionDetails = async function (socket, data) {
|
|
148
|
-
if (!data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) {
|
|
181
|
+
if (!socket.uid || !data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) {
|
|
149
182
|
throw new Error('Invalid request');
|
|
150
183
|
}
|
|
151
|
-
const [poll, option
|
|
184
|
+
const [poll, option] = await Promise.all([
|
|
152
185
|
Poll.getInfo(data.pollId),
|
|
153
186
|
Poll.getOption(data.pollId, data.optionId, true),
|
|
154
|
-
Config.getSettings(),
|
|
155
187
|
]);
|
|
156
188
|
if (!poll) {
|
|
157
189
|
throw new Error('[[error:invalid-data]]');
|
|
158
190
|
}
|
|
159
|
-
const allowAnon = settings.allowGuestsToViewResults === 'on';
|
|
160
191
|
const canRead = await privileges.posts.can('topics:read', poll.pid, socket.uid);
|
|
161
|
-
if (!canRead
|
|
192
|
+
if (!canRead) {
|
|
162
193
|
throw new Error('[[error:no-privileges]]');
|
|
163
194
|
}
|
|
164
195
|
if (!option || !option.votes || !option.votes.length) {
|
|
@@ -170,7 +201,7 @@ Sockets.getOptionDetails = async function (socket, data) {
|
|
|
170
201
|
}
|
|
171
202
|
|
|
172
203
|
const userData = await NodeBB.User.getUsersFields(option.votes, [
|
|
173
|
-
'uid', 'username', 'userslug', 'picture',
|
|
204
|
+
'uid', 'username', 'userslug', 'picture', 'displayname', 'icon:text', 'icon:bgColor',
|
|
174
205
|
]);
|
|
175
206
|
option.votes = userData;
|
|
176
207
|
|
|
@@ -205,7 +236,7 @@ async function anonymizeVoters(callerUid, pollId, options) {
|
|
|
205
236
|
}
|
|
206
237
|
const uidToIsAnon = _.zipObject(uids, isAnon);
|
|
207
238
|
options.forEach((opt) => {
|
|
208
|
-
opt.votes = opt.votes.map(
|
|
239
|
+
opt.votes = (opt.votes || []).map(
|
|
209
240
|
uid => (uidToIsAnon[uid] && String(callerUid) !== uid ? 0 : uid)
|
|
210
241
|
);
|
|
211
242
|
});
|
|
@@ -218,3 +249,77 @@ async function checkPrivs(cid, socketUid) {
|
|
|
218
249
|
}
|
|
219
250
|
return true;
|
|
220
251
|
}
|
|
252
|
+
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// FEP-9967 outbound vote federation
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
// Build a FEP-9967 vote Note. Stable, idempotent id so re-sends and Undos
|
|
258
|
+
// dedupe remotely.
|
|
259
|
+
function buildVoteNote({ voterUrl, questionId, optionName, pollAuthorUrl }) {
|
|
260
|
+
const voteId = `${voterUrl}#activity/vote/${encodeURIComponent(questionId)}/${encodeURIComponent(optionName)}`;
|
|
261
|
+
return {
|
|
262
|
+
id: voteId,
|
|
263
|
+
type: 'Note',
|
|
264
|
+
attributedTo: voterUrl,
|
|
265
|
+
inReplyTo: questionId,
|
|
266
|
+
name: optionName,
|
|
267
|
+
to: [pollAuthorUrl],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Federate a vote change (additions and/or removals) for a remote poll.
|
|
272
|
+
// Anonymous votes are not federated (FEP votes are attributed to the voter
|
|
273
|
+
// actor, which anon votes lack).
|
|
274
|
+
async function federateVoteChange({ pollData, socket, voteAnon, removedOptionIds, addedOptionIds }) {
|
|
275
|
+
if (voteAnon) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!removedOptionIds.length && !addedOptionIds.length) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const activitypub = nodebb.require('./src/activitypub');
|
|
283
|
+
const options = JSON.parse(pollData.options || '[]');
|
|
284
|
+
const [questionId, pollAuthorUrl] = await Promise.all([
|
|
285
|
+
posts.getPostField(pollData.pid, 'url'), // remote Question id (a URL)
|
|
286
|
+
posts.getPostField(pollData.pid, 'uid'), // remote actor URL (poll author)
|
|
287
|
+
]);
|
|
288
|
+
const voterUrl = `${nconf.get('url')}/uid/${socket.uid}`;
|
|
289
|
+
|
|
290
|
+
const toName = (id) => {
|
|
291
|
+
const opt = options.find(o => String(o.id) === String(id));
|
|
292
|
+
return opt ? opt.title : null;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Undo removed options
|
|
296
|
+
for (const id of removedOptionIds) {
|
|
297
|
+
const name = toName(id);
|
|
298
|
+
if (!name) continue;
|
|
299
|
+
const voteNote = buildVoteNote({ voterUrl, questionId, optionName: name, pollAuthorUrl });
|
|
300
|
+
// eslint-disable-next-line no-await-in-loop
|
|
301
|
+
await activitypub.send('uid', socket.uid, [pollAuthorUrl], {
|
|
302
|
+
id: `${voteNote.id}#undo`,
|
|
303
|
+
type: 'Undo',
|
|
304
|
+
object: {
|
|
305
|
+
id: voteNote.id,
|
|
306
|
+
type: 'Create',
|
|
307
|
+
actor: voterUrl,
|
|
308
|
+
object: voteNote,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Create added options
|
|
314
|
+
for (const id of addedOptionIds) {
|
|
315
|
+
const name = toName(id);
|
|
316
|
+
if (!name) continue;
|
|
317
|
+
const voteNote = buildVoteNote({ voterUrl, questionId, optionName: name, pollAuthorUrl });
|
|
318
|
+
// eslint-disable-next-line no-await-in-loop
|
|
319
|
+
await activitypub.send('uid', socket.uid, [pollAuthorUrl], {
|
|
320
|
+
id: voteNote.id,
|
|
321
|
+
type: 'Create',
|
|
322
|
+
object: voteNote,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
package/lib/vote.js
CHANGED
|
@@ -73,6 +73,10 @@ Vote.canUpdateVote = async function (uid, pollId) {
|
|
|
73
73
|
if (!pollInfo) {
|
|
74
74
|
throw new Error('[[error:invalid-data]]');
|
|
75
75
|
}
|
|
76
|
+
// Remote polls: vote changes/removals are not federated (no OSW support yet)
|
|
77
|
+
if (parseInt(pollInfo.remote, 10) === 1) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
76
80
|
const [canRead, ended, deleted, updateDisabled] = await Promise.all([
|
|
77
81
|
privileges.posts.can('topics:read', pollInfo.pid, uid),
|
|
78
82
|
Poll.hasEnded(pollId),
|
package/library.js
CHANGED
|
@@ -92,7 +92,7 @@ Plugin.renderPollWidget = async function (widget) {
|
|
|
92
92
|
const Poll = require('./lib/poll');
|
|
93
93
|
|
|
94
94
|
const settings = await Config.getSettings();
|
|
95
|
-
const allowAnon = settings.allowGuestsToViewResults
|
|
95
|
+
const allowAnon = !!settings.allowGuestsToViewResults;
|
|
96
96
|
|
|
97
97
|
if (!widget.uid && !allowAnon) {
|
|
98
98
|
return null;
|