nodebb-plugin-poll 4.1.1 → 4.2.1

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 CHANGED
@@ -1,8 +1,8 @@
1
- root = true
2
-
3
- [{*.js, *.css, *.tpl, *.json}]
4
- indent_style = tab
5
- end_of_line = lf
6
- charset = utf-8
7
- trim_trailing_whitespace = true
1
+ root = true
2
+
3
+ [{*.js, *.css, *.tpl, *.json}]
4
+ indent_style = tab
5
+ end_of_line = lf
6
+ charset = utf-8
7
+ trim_trailing_whitespace = true
8
8
  insert_final_newline = false
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2015 Erik Schamper
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Erik Schamper
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
package/README.md CHANGED
@@ -51,7 +51,7 @@ Limitations:
51
51
 
52
52
  - Only the **first** poll on a post is federated (FEP-9967 models one `Question` per poll); multiple polls per post are not supported.
53
53
  - **Anonymous votes are not federated**, since FEP-9967 votes are attributed to the voter's actor.
54
- - Votes on **remote** polls cannot be changed or removed (only new votes are federated).
54
+ - Votes on **remote** polls can be changed or removed (federated as `Create`/`Delete` per FEP-9967), but not all servers support this — e.g. some ignore `Delete` of a vote — so the change may not be reflected on the remote poll.
55
55
 
56
56
  ## Installation
57
57
 
File without changes
@@ -38,6 +38,7 @@
38
38
  "error.invalid-post": "Invalid post.",
39
39
  "error.privilege.create": "You're not allowed to create a poll",
40
40
  "error.anon-voting-not-allowed": "This poll does not allow voting anonymously",
41
+ "error.already-voting": "You are already voting for this poll",
41
42
 
42
43
  "warning.redactor": "You're using Redactor. Do <strong>not</strong> edit the markup manually. Instead, re-open the poll creator.",
43
44
 
@@ -51,6 +52,7 @@
51
52
  "voting_unavailable_message": "This poll has ended or has been marked as deleted. You can still view the results.",
52
53
  "voting_update_disallowed_message": "You have already voted and changing your vote is not allowed for this poll",
53
54
  "vote_is_final": "Changing your vote is not allowed for this poll so your vote is final",
55
+ "remote_vote_caveat": "This poll is from another server. Not all servers support changing or removing a vote, so your update may not be reflected there.",
54
56
 
55
57
  "x-users-voted-for-this-option": "%1 user(s) voted for this option",
56
58
  "votes": "votes",
File without changes
File without changes
File without changes
package/lib/config.js CHANGED
File without changes
package/lib/hooks.js CHANGED
File without changes
package/lib/nodebb.js CHANGED
File without changes
package/lib/poll.js CHANGED
File without changes
package/lib/sockets.js CHANGED
@@ -53,42 +53,50 @@ Sockets.vote = async function (socket, data) {
53
53
 
54
54
  data.uid = socket.uid;
55
55
 
56
- const [pollData, canVote, optionsFilter] = await Promise.all([
57
- db.getObject(`poll:${data.pollId}`),
58
- Vote.canVote(socket.uid, data.pollId),
59
- Poll.hasOptions(data.pollId, data.options),
60
- ]);
61
-
62
- // Filter the options on their existence
63
- data.options = data.options.filter((el, index) => optionsFilter[index]);
56
+ const count = await db.incrObjectField(`poll:voteLocks`, `${data.pollId}:${socket.uid}`);
57
+ if (count > 1) {
58
+ throw new Error('[[poll:error.already-voting]]');
59
+ }
60
+ try {
61
+ const [pollData, canVote, optionsFilter] = await Promise.all([
62
+ db.getObject(`poll:${data.pollId}`),
63
+ Vote.canVote(socket.uid, data.pollId),
64
+ Poll.hasOptions(data.pollId, data.options),
65
+ ]);
66
+
67
+ // Filter the options on their existence
68
+ data.options = data.options.filter((el, index) => optionsFilter[index]);
69
+
70
+ // Give an error if there are too many votes
71
+ if (data.options.length > parseInt(pollData.maximumVotesPerUser, 10)) {
72
+ throw new Error(`You can only vote for ${pollData.maximumVotesPerUser} options on this poll.`);
73
+ }
64
74
 
65
- // Give an error if there are too many votes
66
- if (data.options.length > parseInt(pollData.maximumVotesPerUser, 10)) {
67
- throw new Error(`You can only vote for ${pollData.maximumVotesPerUser} options on this poll.`);
68
- }
75
+ if (data.voteAnon && parseInt(pollData.allowAnonVoting, 10) !== 1) {
76
+ throw new Error('[[poll:error.anon-voting-not-allowed]]');
77
+ }
69
78
 
70
- if (data.voteAnon && parseInt(pollData.allowAnonVoting, 10) !== 1) {
71
- throw new Error('[[poll:error.anon-voting-not-allowed]]');
72
- }
79
+ if (!canVote || !data.options.length) {
80
+ throw new Error('Already voted or invalid option');
81
+ }
73
82
 
74
- if (!canVote || !data.options.length) {
75
- throw new Error('Already voted or invalid option');
76
- }
83
+ await Vote.add(data);
77
84
 
78
- await Vote.add(data);
85
+ // Federate the vote out (FEP-9967) if this is a remote poll
86
+ if (parseInt(pollData.remote, 10) === 1) {
87
+ await federateVoteChange({
88
+ pollData, socket, voteAnon: data.voteAnon, removedOptionIds: [], addedOptionIds: data.options,
89
+ });
90
+ }
79
91
 
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,
92
+ const returnPoll = await Poll.get(data.pollId, socket.uid, false);
93
+ NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
94
+ pollData: returnPoll,
95
+ uid: socket.uid,
84
96
  });
97
+ } finally {
98
+ await db.deleteObjectField(`poll:voteLocks`, `${data.pollId}:${socket.uid}`);
85
99
  }
86
-
87
- const returnPoll = await Poll.get(data.pollId, socket.uid, false);
88
- NodeBB.SocketIndex.server.sockets.emit('event:poll.voteChange', {
89
- pollData: returnPoll,
90
- uid: socket.uid,
91
- });
92
100
  };
93
101
 
94
102
  Sockets.updateVote = async function (socket, data) {
@@ -122,9 +130,10 @@ Sockets.updateVote = async function (socket, data) {
122
130
  throw new Error('Invalid option');
123
131
  }
124
132
 
125
- // Capture the prior vote so we can federate the diff (remote polls only)
133
+ // Capture the prior vote + anon flag so we can federate the diff (remote polls only)
126
134
  const isRemote = parseInt(pollData.remote, 10) === 1;
127
135
  const oldOptions = isRemote ? (await Vote.get({ uid: socket.uid, pollId: data.pollId })).options : [];
136
+ const voteAnon = isRemote ? await db.isSortedSetMember(`poll:${data.pollId}:anon:voters`, socket.uid) : false;
128
137
 
129
138
  await Vote.update(data);
130
139
 
@@ -133,7 +142,7 @@ Sockets.updateVote = async function (socket, data) {
133
142
  const oldSet = oldOptions.map(String);
134
143
  const removedOptionIds = oldOptions.filter(id => !data.options.map(String).includes(String(id)));
135
144
  const addedOptionIds = data.options.filter(id => !oldSet.includes(String(id)));
136
- await federateVoteChange({ pollData, socket, voteAnon: data.voteAnon, removedOptionIds, addedOptionIds });
145
+ await federateVoteChange({ pollData, socket, voteAnon, removedOptionIds, addedOptionIds });
137
146
  }
138
147
 
139
148
  const returnPoll = await Poll.get(data.pollId, socket.uid, false);
@@ -254,8 +263,8 @@ async function checkPrivs(cid, socketUid) {
254
263
  // FEP-9967 outbound vote federation
255
264
  // ---------------------------------------------------------------------------
256
265
 
257
- // Build a FEP-9967 vote Note. Stable, idempotent id so re-sends and Undos
258
- // dedupe remotely.
266
+ // Build a FEP-9967 vote Note. Stable, idempotent id so re-sends and
267
+ // Deletes dedupe remotely.
259
268
  function buildVoteNote({ voterUrl, questionId, optionName, pollAuthorUrl }) {
260
269
  const voteId = `${voterUrl}#activity/vote/${encodeURIComponent(questionId)}/${encodeURIComponent(optionName)}`;
261
270
  return {
@@ -292,21 +301,16 @@ async function federateVoteChange({ pollData, socket, voteAnon, removedOptionIds
292
301
  return opt ? opt.title : null;
293
302
  };
294
303
 
295
- // Undo removed options
304
+ // Delete removed options (the vote Note is an entity; retraction deletes it)
296
305
  for (const id of removedOptionIds) {
297
306
  const name = toName(id);
298
307
  if (!name) continue;
299
308
  const voteNote = buildVoteNote({ voterUrl, questionId, optionName: name, pollAuthorUrl });
300
309
  // eslint-disable-next-line no-await-in-loop
301
310
  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
- },
311
+ id: `${voteNote.id}#delete`,
312
+ type: 'Delete',
313
+ object: voteNote,
310
314
  });
311
315
  }
312
316
 
package/lib/vote.js CHANGED
@@ -73,10 +73,6 @@ 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
- }
80
76
  const [canRead, ended, deleted, updateDisabled] = await Promise.all([
81
77
  privileges.posts.can('topics:read', pollInfo.pid, uid),
82
78
  Poll.hasEnded(pollId),
package/library.js CHANGED
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const nconf = nodebb.require('nconf');
4
+
3
5
  const NodeBB = require('./lib/nodebb');
4
6
  const Config = require('./lib/config');
5
7
  const Sockets = require('./lib/sockets');
@@ -47,6 +49,10 @@ Plugin.load = async function (params) {
47
49
  NodeBB.PluginSockets[Config.plugin.id] = Sockets;
48
50
 
49
51
  NodeBB.app = params.app;
52
+
53
+ if (nconf.get('runJobs')) {
54
+ await db.delete(`poll:voteLocks`);
55
+ }
50
56
  };
51
57
 
52
58
  Plugin.addAdminNavigation = function (adminHeader) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-poll",
3
- "version": "4.1.1",
3
+ "version": "4.2.1",
4
4
  "description": "NodeBB Poll Plugin",
5
5
  "main": "library.js",
6
6
  "scripts": {
package/plugin.json CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -204,10 +204,6 @@
204
204
  };
205
205
 
206
206
  View.prototype.voteUpdateAllowed = function () {
207
- // Remote polls: vote changes/removals are not federated (no OSW support yet)
208
- if (parseInt(this.pollData.info.remote, 10) === 1) {
209
- return false;
210
- }
211
207
  return parseInt(this.pollData.info.disallowVoteUpdate, 10) !== 1;
212
208
  };
213
209
 
File without changes
File without changes
@@ -14,3 +14,9 @@
14
14
  {{tx("poll:vote_is_final")}}
15
15
  </div>
16
16
  {{{ end }}}
17
+
18
+ {{{ if poll.info.remote }}}
19
+ <div class="alert alert-info" role="alert">
20
+ {{tx("poll:remote_vote_caveat")}}
21
+ </div>
22
+ {{{ end }}}
@@ -1,15 +1,15 @@
1
- 'use strict';
2
-
3
- const db = nodebb.require('./src/database');
4
- const privileges = nodebb.require('./src/privileges');
5
-
6
- module.exports = {
7
- name: 'Fix incorrectly given poll privilege',
8
- timestamp: Date.UTC(2020, 9, 29),
9
- method: async () => {
10
- const cids = await db.getSortedSetRange('categories:cid', 0, -1);
11
- await Promise.all(cids.map(async (cid) => {
12
- await privileges.categories.rescind(['poll:create'], cid, ['registered-users']);
13
- }));
14
- },
15
- };
1
+ 'use strict';
2
+
3
+ const db = nodebb.require('./src/database');
4
+ const privileges = nodebb.require('./src/privileges');
5
+
6
+ module.exports = {
7
+ name: 'Fix incorrectly given poll privilege',
8
+ timestamp: Date.UTC(2020, 9, 29),
9
+ method: async () => {
10
+ const cids = await db.getSortedSetRange('categories:cid', 0, -1);
11
+ await Promise.all(cids.map(async (cid) => {
12
+ await privileges.categories.rescind(['poll:create'], cid, ['registered-users']);
13
+ }));
14
+ },
15
+ };