nodebb-plugin-mentions 2.15.2 → 3.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/.eslintrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "nodebb/lib"
3
+ }
package/controllers.js CHANGED
@@ -1,14 +1,10 @@
1
- 'use strict';
2
-
3
- var groups = require.main.require('./src/groups');
4
-
5
- var Controllers = module.exports;
6
-
7
- Controllers.renderAdminPage = function (req, res, next) {
8
- groups.getGroupsFromSet('groups:visible:createtime', 0, -1, function(err, groupData) {
9
- if (err) {
10
- return next(err);
11
- }
12
- res.render('admin/plugins/mentions', { groups: groupData });
13
- });
14
- };
1
+ 'use strict';
2
+
3
+ const groups = require.main.require('./src/groups');
4
+
5
+ const Controllers = module.exports;
6
+
7
+ Controllers.renderAdminPage = async function (req, res) {
8
+ const groupData = await groups.getGroupsFromSet('groups:visible:createtime', 0, -1);
9
+ res.render('admin/plugins/mentions', { groups: groupData });
10
+ };
package/lib/utility.js CHANGED
@@ -1,16 +1,18 @@
1
- const Utility = module.exports;
2
-
3
- Utility.split = function(input, isMarkdown, splitBlockquote, splitCode) {
4
- if (!input) {
5
- return [];
6
- }
7
-
8
- var matchers = [isMarkdown ? '\\[.*?\\]\\(.*?\\)' : '<a\\s[\\s\\S]*?</a>(?=<[^>]+>)?'];
9
- if (splitBlockquote) {
10
- matchers.push(isMarkdown ? '^>.*$' : '^<blockquote>.*?</blockquote>');
11
- }
12
- if (splitCode) {
13
- matchers.push(isMarkdown ? '`[^`\n]+`|```[\\s\\S]+```' : '<code[\\s\\S]*?</code>|<pre[\\s\\S]*?</pre>');
14
- }
15
- return input.split(new RegExp('(' + matchers.join('|') + ')', 'gm'));
16
- }
1
+ 'use strict';
2
+
3
+ const Utility = module.exports;
4
+
5
+ Utility.split = function (input, isMarkdown, splitBlockquote, splitCode) {
6
+ if (!input) {
7
+ return [];
8
+ }
9
+
10
+ const matchers = [isMarkdown ? '\\[.*?\\]\\(.*?\\)' : '<a\\s[\\s\\S]*?</a>(?=<[^>]+>)?'];
11
+ if (splitBlockquote) {
12
+ matchers.push(isMarkdown ? '^>.*$' : '^<blockquote>.*?</blockquote>');
13
+ }
14
+ if (splitCode) {
15
+ matchers.push(isMarkdown ? '`[^`\n]+`|```[\\s\\S]+```' : '<code[\\s\\S]*?</code>|<pre[\\s\\S]*?</pre>');
16
+ }
17
+ return input.split(new RegExp(`(${matchers.join('|')})`, 'gm'));
18
+ };
package/library.js CHANGED
@@ -1,54 +1,54 @@
1
+ /* eslint-disable no-await-in-loop */
2
+
1
3
  'use strict';
2
4
 
3
- var async = require('async');
4
- var winston = module.parent.require('winston');
5
- var XRegExp = require('xregexp');
6
- var validator = require('validator');
7
- var nconf = module.parent.require('nconf');
8
-
9
- var db = require.main.require('./src/database');
10
- var api = require.main.require('./src/api');
11
- var Topics = require.main.require('./src/topics');
12
- var posts = require.main.require('./src/posts');
13
- var User = require.main.require('./src/user');
14
- var Groups = require.main.require('./src/groups');
15
- var Notifications = require.main.require('./src/notifications');
16
- var Privileges = require.main.require('./src/privileges');
17
- var plugins = require.main.require('./src/plugins');
18
- var Meta = require.main.require('./src/meta');
19
- var slugify = require.main.require('./src/slugify');
20
- var batch = require.main.require('./src/batch');
21
- const utils = require.main.require('./public/src/utils');
22
-
23
- var SocketPlugins = require.main.require('./src/socket.io/plugins');
5
+ const _ = require('lodash');
6
+ const XRegExp = require('xregexp');
7
+ const validator = require('validator');
8
+ const entitiesDecode = require('html-entities').decode;
9
+
10
+ const nconf = require.main.require('nconf');
11
+ const winston = require.main.require('winston');
12
+
13
+ const db = require.main.require('./src/database');
14
+ const api = require.main.require('./src/api');
15
+ const Topics = require.main.require('./src/topics');
16
+ const posts = require.main.require('./src/posts');
17
+ const User = require.main.require('./src/user');
18
+ const Groups = require.main.require('./src/groups');
19
+ const Notifications = require.main.require('./src/notifications');
20
+ const Privileges = require.main.require('./src/privileges');
21
+ const plugins = require.main.require('./src/plugins');
22
+ const Meta = require.main.require('./src/meta');
23
+ const slugify = require.main.require('./src/slugify');
24
+ const batch = require.main.require('./src/batch');
25
+ const utils = require.main.require('./src/utils');
26
+ const SocketPlugins = require.main.require('./src/socket.io/plugins');
24
27
 
25
28
  const utility = require('./lib/utility');
26
29
 
27
- var regex = XRegExp('(?:^|\\s|\\>|;)(@[\\p{L}\\d\\-_.]+)', 'g');
28
- var isLatinMention = /@[\w\d\-_.]+$/;
29
- var removePunctuationSuffix = function(string) {
30
- return string.replace(/[!?.]*$/, '');
31
- };
32
- var entitiesDecode = require('html-entities').decode;
33
-
34
- var Mentions = {
35
- _settings: {},
36
- _defaults: {
37
- disableFollowedTopics: 'off',
38
- autofillGroups: 'off',
39
- disableGroupMentions: '[]',
40
- overrideIgnores: 'off',
41
- display: '',
42
- }
30
+ const regex = XRegExp('(?:^|\\s|\\>|;)(@[\\p{L}\\d\\-_.]+)', 'g');
31
+ const isLatinMention = /@[\w\d\-_.]+$/;
32
+
33
+ const Mentions = module.exports;
34
+
35
+ Mentions._settings = {};
36
+ Mentions._defaults = {
37
+ disableFollowedTopics: 'off',
38
+ autofillGroups: 'off',
39
+ disableGroupMentions: '[]',
40
+ overrideIgnores: 'off',
41
+ display: '',
43
42
  };
43
+
44
44
  SocketPlugins.mentions = {};
45
45
 
46
46
  Mentions.init = async (data) => {
47
- var hostMiddleware = require.main.require('./src/middleware');
48
- var controllers = require('./controllers');
47
+ const hostMiddleware = require.main.require('./src/middleware');
48
+ const routeHelpers = require.main.require('./src/routes/helpers');
49
+ const controllers = require('./controllers');
49
50
 
50
- data.router.get('/admin/plugins/mentions', hostMiddleware.admin.buildHeader, controllers.renderAdminPage);
51
- data.router.get('/api/admin/plugins/mentions', controllers.renderAdminPage);
51
+ routeHelpers.setupAdminPageRoute(data.router, '/admin/plugins/mentions', hostMiddleware, [], controllers.renderAdminPage);
52
52
 
53
53
  // Retrieve settings
54
54
  Object.assign(Mentions._settings, Mentions._defaults, await Meta.settings.get('mentions'));
@@ -57,14 +57,14 @@ Mentions.init = async (data) => {
57
57
  Mentions.addAdminNavigation = async (header) => {
58
58
  header.plugins.push({
59
59
  route: '/plugins/mentions',
60
- name: 'Mentions'
60
+ name: 'Mentions',
61
61
  });
62
62
 
63
63
  return header;
64
64
  };
65
65
 
66
66
  function getNoMentionGroups() {
67
- var noMentionGroups = ['registered-users', 'verified-users', 'unverified-users', 'guests'];
67
+ let noMentionGroups = ['registered-users', 'verified-users', 'unverified-users', 'guests'];
68
68
  try {
69
69
  noMentionGroups = noMentionGroups.concat(JSON.parse(Mentions._settings.disableGroupMentions));
70
70
  } catch (err) {
@@ -73,121 +73,108 @@ function getNoMentionGroups() {
73
73
  return noMentionGroups;
74
74
  }
75
75
 
76
- Mentions.notify = function(data) {
77
- var postData = data.post;
78
- var cleanedContent = Mentions.clean(postData.content, true, true, true);
79
- var matches = cleanedContent.match(regex);
80
-
76
+ Mentions.notify = async function (data) {
77
+ const postData = data.post;
78
+ const postOwner = parseInt(postData.uid, 10);
79
+ const cleanedContent = Mentions.clean(postData.content, true, true, true);
80
+ let matches = cleanedContent.match(regex);
81
81
  if (!matches) {
82
82
  return;
83
83
  }
84
84
 
85
- var noMentionGroups = getNoMentionGroups();
86
-
87
- matches = matches.map(function(match) {
88
- return slugify(match);
89
- }).filter(function(match, index, array) {
90
- return match && array.indexOf(match) === index && noMentionGroups.indexOf(match) === -1;
91
- });
92
-
85
+ const noMentionGroups = getNoMentionGroups();
86
+ matches = _.uniq(matches.map(match => slugify(match))).filter(match => match && !noMentionGroups.includes(match));
93
87
  if (!matches.length) {
94
88
  return;
95
89
  }
96
90
 
97
- async.parallel({
98
- userRecipients: function(next) {
99
- async.filter(matches, User.existsBySlug, next);
100
- },
101
- groupRecipients: function(next) {
102
- async.filter(matches, Groups.existsBySlug, next);
103
- }
104
- }, function(err, results) {
105
- if (err) {
106
- return;
107
- }
108
-
109
- if (!results.userRecipients.length && !results.groupRecipients.length) {
110
- return;
111
- }
112
-
113
- async.parallel({
114
- topic: function(next) {
115
- Topics.getTopicFields(postData.tid, ['title', 'cid'], next);
116
- },
117
- author: function(next) {
118
- User.getUserField(postData.uid, 'username', next);
119
- },
120
- uids: function(next) {
121
- async.map(results.userRecipients, function(slug, next) {
122
- User.getUidByUserslug(slug, next);
123
- }, next);
124
- },
125
- groupData: async function() {
126
- return await getGroupMemberUids(results.groupRecipients);
127
- },
128
- topicFollowers: function(next) {
129
- if (Mentions._settings.disableFollowedTopics === 'on') {
130
- Topics.getFollowers(postData.tid, next);
131
- } else {
132
- next(null, []);
133
- }
134
- }
135
- }, async (err, results) => {
136
- if (err) {
137
- return;
138
- }
91
+ const [uidsToNotify, groupsToNotify] = await Promise.all([
92
+ getUidsToNotify(matches),
93
+ getGroupsToNotify(matches),
94
+ ]);
139
95
 
140
- var title = entitiesDecode(results.topic.title);
141
- var titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
96
+ if (!uidsToNotify.length && !groupsToNotify.length) {
97
+ return;
98
+ }
142
99
 
143
- var uids = results.uids.map(String).filter(function(uid, index, array) {
144
- return array.indexOf(uid) === index && parseInt(uid, 10) !== parseInt(postData.uid, 10) && !results.topicFollowers.includes(uid);
145
- });
100
+ const [topic, author, topicFollowers] = await Promise.all([
101
+ Topics.getTopicFields(postData.tid, ['title', 'cid']),
102
+ User.getUserField(postData.uid, 'username'),
103
+ Mentions._settings.disableFollowedTopics === 'on' ? Topics.getFollowers(postData.tid) : [],
104
+ ]);
146
105
 
147
- if (Mentions._settings.privilegedDirectReplies === 'on') {
148
- const toPid = await posts.getPostField(data.post.pid, 'toPid');
149
- uids = await filterPrivilegedUids(uids, data.post.cid, toPid);
150
- }
106
+ const title = entitiesDecode(topic.title);
107
+ const titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
151
108
 
152
- var groupMemberUids = {};
153
- results.groupData.groupNames.forEach(function(groupName, index) {
154
- results.groupData.groupMembers[index] = results.groupData.groupMembers[index].filter(function(uid) {
155
- if (!uid || groupMemberUids[uid]) {
156
- return false;
157
- }
158
- groupMemberUids[uid] = 1;
159
- return !uids.includes(uid) &&
160
- parseInt(uid, 10) !== parseInt(postData.uid, 10) &&
161
- !results.topicFollowers.includes(uid);
162
- });
163
- });
109
+ let uids = uidsToNotify.filter(
110
+ uid => parseInt(uid, 10) !== postOwner && !topicFollowers.includes(uid)
111
+ );
164
112
 
165
- const filteredUids = await filterUidsAlreadyMentioned(uids, postData.pid);
113
+ if (Mentions._settings.privilegedDirectReplies === 'on') {
114
+ const toPid = await posts.getPostField(data.post.pid, 'toPid');
115
+ uids = await filterPrivilegedUids(uids, data.post.cid, toPid);
116
+ }
166
117
 
167
- if (filteredUids.length) {
168
- sendNotificationToUids(postData, filteredUids, 'user', '[[notifications:user_mentioned_you_in, ' + results.author + ', ' + titleEscaped + ']]');
169
- await db.setAdd(`mentions:pid:${postData.pid}:uids`, filteredUids);
118
+ const groupMemberUids = {};
119
+ groupsToNotify.forEach((groupData) => {
120
+ groupData.members = groupData.members.filter((uid) => {
121
+ if (!uid || groupMemberUids[uid]) {
122
+ return false;
170
123
  }
171
-
172
- for (let i = 0; i < results.groupData.groupNames.length; ++i) {
173
- const memberUids = results.groupData.groupMembers[i];
174
- const groupName = results.groupData.groupNames[i];
175
- const groupMentionSent = await db.isSetMember(`mentions:pid:${postData.pid}:groups`, groupName);
176
- if (!groupMentionSent && memberUids.length) {
177
- sendNotificationToUids(postData, memberUids, groupName, '[[notifications:user_mentioned_group_in, ' + results.author + ', ' + groupName + ', ' + titleEscaped + ']]');
178
- await db.setAdd(`mentions:pid:${postData.pid}:groups`, groupName);
179
- }
180
- };
124
+ groupMemberUids[uid] = 1;
125
+ return !uids.includes(uid) &&
126
+ parseInt(uid, 10) !== postOwner &&
127
+ !topicFollowers.includes(uid);
181
128
  });
182
129
  });
130
+
131
+ const filteredUids = await filterUidsAlreadyMentioned(uids, postData.pid);
132
+ if (filteredUids.length) {
133
+ await sendNotificationToUids(postData, filteredUids, 'user', `[[notifications:user_mentioned_you_in, ${author}, ${titleEscaped}]]`);
134
+ await db.setAdd(`mentions:pid:${postData.pid}:uids`, filteredUids);
135
+ }
136
+
137
+ for (let i = 0; i < groupsToNotify.length; ++i) {
138
+ if (groupsToNotify[i] && groupsToNotify[i].name && groupsToNotify[i].members) {
139
+ const memberUids = groupsToNotify[i].members;
140
+ const groupName = groupsToNotify[i].name;
141
+ const groupMentionSent = await db.isSetMember(`mentions:pid:${postData.pid}:groups`, groupName);
142
+ if (!groupMentionSent && memberUids.length) {
143
+ await sendNotificationToUids(postData, memberUids, groupName, `[[notifications:user_mentioned_group_in, ${author} , ${groupName}, ${titleEscaped}]]`);
144
+ await db.setAdd(`mentions:pid:${postData.pid}:groups`, groupName);
145
+ }
146
+ }
147
+ }
183
148
  };
184
149
 
150
+ async function getUidsToNotify(matches) {
151
+ const uids = await db.sortedSetScores('userslug:uid', matches);
152
+ return _.uniq(uids.filter(Boolean).map(String));
153
+ }
154
+
155
+ async function getGroupsToNotify(matches) {
156
+ if (!matches.length) {
157
+ return [];
158
+ }
159
+ const groupNames = Object.values(await db.getObjectFields('groupslug:groupname', matches));
160
+ const groupMembers = await Promise.all(groupNames.map(async (groupName) => {
161
+ if (!groupName) {
162
+ return [];
163
+ }
164
+ return db.getSortedSetRange(`group:${groupName}:members`, 0, 999);
165
+ }));
166
+ return groupNames.map((groupName, i) => ({
167
+ name: groupName,
168
+ members: groupMembers[i],
169
+ }));
170
+ }
171
+
185
172
  Mentions.actionPostPurge = async (hookData) => {
186
173
  await db.deleteAll([
187
174
  `mentions:pid:${hookData.postData.pid}:uids`,
188
175
  `mentions:pid:${hookData.postData.pid}:groups`,
189
176
  ]);
190
- }
177
+ };
191
178
 
192
179
  async function filterUidsAlreadyMentioned(uids, pid) {
193
180
  const isMember = await db.isSetMembers(`mentions:pid:${pid}:uids`, uids);
@@ -211,97 +198,48 @@ Mentions.addFields = async (data) => {
211
198
  return data;
212
199
  };
213
200
 
214
- function sendNotificationToUids(postData, uids, nidType, notificationText) {
201
+ async function sendNotificationToUids(postData, uids, nidType, notificationText) {
215
202
  if (!uids.length) {
216
203
  return;
217
204
  }
218
205
 
219
- var filteredUids = [];
220
- var notification;
221
- async.waterfall([
222
- function (next) {
223
- createNotification(postData, nidType, notificationText, next);
224
- },
225
- function (_notification, next) {
226
- notification = _notification;
227
- if (!notification) {
228
- return next();
229
- }
230
-
231
- batch.processArray(uids, function (uids, next) {
232
- async.waterfall([
233
- function(next) {
234
- Privileges.topics.filterUids('read', postData.tid, uids, next);
235
- },
236
- function(_uids, next) {
237
- if (Mentions._settings.overrideIgnores === 'on') {
238
- return setImmediate(next, null, _uids);
239
- }
240
-
241
- Topics.filterIgnoringUids(postData.tid, _uids, next);
242
- },
243
- function(_uids, next) {
244
- if (!_uids.length) {
245
- return next();
246
- }
247
-
248
- filteredUids = filteredUids.concat(_uids);
249
-
250
- next();
251
- }
252
- ], next);
253
- }, {
254
- interval: 1000,
255
- batch: 500,
256
- }, next);
257
- },
258
- ], function (err) {
259
- if (err) {
260
- return winston.error(err);
261
- }
206
+ const filteredUids = [];
207
+ const notification = await createNotification(postData, nidType, notificationText);
208
+ if (!notification) {
209
+ return;
210
+ }
262
211
 
263
- if (notification && filteredUids.length) {
264
- plugins.hooks.fire('action:mentions.notify', { notification, uids: filteredUids });
265
- Notifications.push(notification, filteredUids);
212
+ await batch.processArray(uids, async (uids) => {
213
+ uids = await Privileges.topics.filterUids('read', postData.tid, uids);
214
+ if (Mentions._settings.overrideIgnores !== 'on') {
215
+ uids = await Topics.filterIgnoringUids(postData.tid, uids);
266
216
  }
217
+ filteredUids.push(...uids);
218
+ }, {
219
+ interval: 1000,
220
+ batch: 500,
267
221
  });
268
- }
269
222
 
270
- function createNotification(postData, nidType, notificationText, callback) {
271
- Topics.getTopicField(postData.tid, 'title', function (err, title) {
272
- if (err) {
273
- return callback(err);
274
- }
275
- if (title) {
276
- title = utils.decodeHTMLEntities(title);
277
- }
278
- Notifications.create({
279
- type: 'mention',
280
- bodyShort: notificationText,
281
- bodyLong: postData.content,
282
- nid: 'tid:' + postData.tid + ':pid:' + postData.pid + ':uid:' + postData.uid + ':' + nidType,
283
- pid: postData.pid,
284
- tid: postData.tid,
285
- from: postData.uid,
286
- path: '/post/' + postData.pid,
287
- topicTitle: title,
288
- importance: 6,
289
- }, callback);
290
- });
223
+ if (notification && filteredUids.length) {
224
+ plugins.hooks.fire('action:mentions.notify', { notification, uids: filteredUids });
225
+ Notifications.push(notification, filteredUids);
226
+ }
291
227
  }
292
228
 
293
- async function getGroupMemberUids(groupRecipients) {
294
- if (!groupRecipients.length) {
295
- return { groupNames: [], groupMembers: [] };
296
- }
297
- const groupNames = Object.values(await db.getObjectFields('groupslug:groupname', groupRecipients));
298
- const groupMembers = await Promise.all(groupNames.map(async (groupName) => {
299
- if (!groupName) {
300
- return [];
301
- }
302
- return db.getSortedSetRange(`group:${groupName}:members`, 0, 999);
303
- }));
304
- return { groupNames, groupMembers };
229
+ async function createNotification(postData, nidType, notificationText) {
230
+ const title = await Topics.getTopicField(postData.tid, 'title');
231
+ return await Notifications.create({
232
+ type: 'mention',
233
+ bodyShort: notificationText,
234
+ bodyLong: postData.content,
235
+ nid: `tid:${postData.tid}:pid:${postData.pid}:uid:${postData.uid}:${nidType}`,
236
+ pid: postData.pid,
237
+ tid: postData.tid,
238
+ from: postData.uid,
239
+ path: `/post/${postData.pid}`,
240
+ topicTitle: title ? utils.decodeHTMLEntities(title) : title,
241
+ importance: 6,
242
+ });
305
243
  }
306
244
 
307
245
  Mentions.parsePost = async (data) => {
@@ -314,11 +252,15 @@ Mentions.parsePost = async (data) => {
314
252
  return data;
315
253
  };
316
254
 
255
+ function removePunctuationSuffix(string) {
256
+ return string.replace(/[!?.]*$/, '');
257
+ }
258
+
317
259
  Mentions.parseRaw = async (content) => {
318
260
  let splitContent = utility.split(content, false, false, true);
319
- var matches = [];
320
- splitContent.forEach(function(cleanedContent, i) {
321
- if ((i & 1) === 0) {
261
+ let matches = [];
262
+ splitContent.forEach((cleanedContent, i) => {
263
+ if ((i % 2) === 0) {
322
264
  matches = matches.concat(cleanedContent.match(regex) || []);
323
265
  }
324
266
  });
@@ -327,21 +269,18 @@ Mentions.parseRaw = async (content) => {
327
269
  return content;
328
270
  }
329
271
 
330
- matches = matches.filter(function(cur, idx) {
331
- // Eliminate duplicates
332
- return idx === matches.indexOf(cur);
333
- }).map(function(match) {
272
+ matches = _.uniq(matches).map((match) => {
334
273
  /**
335
274
  * Javascript-favour of regex does not support lookaround,
336
275
  * so need to clean up the cruft by discarding everthing
337
276
  * before the @
338
277
  */
339
- var atIndex = match.indexOf('@');
278
+ const atIndex = match.indexOf('@');
340
279
  return atIndex !== 0 ? match.slice(atIndex) : match;
341
280
  });
342
281
 
343
282
  await Promise.all(matches.map(async (match) => {
344
- var slug = slugify(match.slice(1));
283
+ const slug = slugify(match.slice(1));
345
284
  match = removePunctuationSuffix(match);
346
285
 
347
286
  const uid = await User.getUidByUserslug(slug);
@@ -351,22 +290,22 @@ Mentions.parseRaw = async (content) => {
351
290
  });
352
291
 
353
292
  if (results.user.uid || results.groupExists) {
354
- var regex = isLatinMention.test(match)
355
- ? new RegExp('(?:^|\\s|\>|;)' + match + '\\b', 'g')
356
- : new RegExp('(?:^|\\s|\>|;)' + match, 'g');
293
+ const regex = isLatinMention.test(match) ?
294
+ new RegExp(`(?:^|\\s|\>|;)${match}\\b`, 'g') :
295
+ new RegExp(`(?:^|\\s|\>|;)${match}`, 'g');
357
296
 
358
297
  let skip = false;
359
298
 
360
- splitContent = splitContent.map(function(c, i) {
299
+ splitContent = splitContent.map((c, i) => {
361
300
  // *Might* not be needed anymore? Check pls...
362
- if (skip || (i & 1) === 1) {
301
+ if (skip || (i % 2) === 1) {
363
302
  skip = c === '<code>'; // if code block detected, skip the content inside of it
364
303
  return c;
365
304
  }
366
- return c.replace(regex, function(match) {
305
+ return c.replace(regex, (match) => {
367
306
  // Again, cleaning up lookaround leftover bits
368
- var atIndex = match.indexOf('@');
369
- var plain = match.slice(0, atIndex);
307
+ const atIndex = match.indexOf('@');
308
+ const plain = match.slice(0, atIndex);
370
309
  match = match.slice(atIndex);
371
310
  if (results.user.uid) {
372
311
  switch (Mentions._settings.display) {
@@ -379,9 +318,9 @@ Mentions.parseRaw = async (content) => {
379
318
  }
380
319
  }
381
320
 
382
- var str = results.user.uid
383
- ? '<a class="plugin-mentions-user plugin-mentions-a" href="' + nconf.get('url') + '/uid/' + results.user.uid + '">' + match + '</a>'
384
- : '<a class="plugin-mentions-group plugin-mentions-a" href="' + nconf.get('url') + '/groups/' + slug + '">' + match + '</a>';
321
+ const str = results.user.uid ?
322
+ `<a class="plugin-mentions-user plugin-mentions-a" href="${nconf.get('url')}/uid/${results.user.uid}">${match}</a>` :
323
+ `<a class="plugin-mentions-group plugin-mentions-a" href="${nconf.get('url')}/groups/${slug}">${match}</a>`;
385
324
 
386
325
  return plain + str;
387
326
  });
@@ -392,19 +331,17 @@ Mentions.parseRaw = async (content) => {
392
331
  return splitContent.join('');
393
332
  };
394
333
 
395
- Mentions.clean = function(input, isMarkdown, stripBlockquote, stripCode) {
396
- var split = utility.split(input, isMarkdown, stripBlockquote, stripCode);
397
- split = split.filter(function(e, i) {
398
- // only keep non-code/non-blockquote
399
- return (i & 1) === 0;
400
- });
334
+ Mentions.clean = function (input, isMarkdown, stripBlockquote, stripCode) {
335
+ let split = utility.split(input, isMarkdown, stripBlockquote, stripCode);
336
+ // only keep non-code/non-blockquote
337
+ split = split.filter((el, i) => (i % 2) === 0);
401
338
  return split.join('');
402
339
  };
403
340
 
404
341
  /*
405
342
  Local utility methods
406
343
  */
407
- async function filterPrivilegedUids (uids, cid, toPid) {
344
+ async function filterPrivilegedUids(uids, cid, toPid) {
408
345
  let toPidUid;
409
346
  if (toPid) {
410
347
  toPidUid = await posts.getPostField(toPid, 'uid');
@@ -428,12 +365,12 @@ async function filterPrivilegedUids (uids, cid, toPid) {
428
365
  return uids.filter(Boolean);
429
366
  }
430
367
 
431
- async function filterDisallowedFullnames (users) {
368
+ async function filterDisallowedFullnames(users) {
432
369
  const userSettings = await User.getMultipleUserSettings(users.map(user => user.uid));
433
370
  return users.filter((user, index) => userSettings[index].showfullname);
434
371
  }
435
372
 
436
- async function stripDisallowedFullnames (users) {
373
+ async function stripDisallowedFullnames(users) {
437
374
  const userSettings = await User.getMultipleUserSettings(users.map(user => user.uid));
438
375
  return users.map((user, index) => {
439
376
  if (!userSettings[index].showfullname) {
@@ -449,36 +386,24 @@ async function stripDisallowedFullnames (users) {
449
386
 
450
387
  SocketPlugins.mentions.getTopicUsers = async (socket, data) => {
451
388
  const uids = await Topics.getUids(data.tid);
452
- const users = await User.getUsers(uids);
389
+ const users = await User.getUsers(uids);
453
390
  if (Meta.config.hideFullname) {
454
391
  return users;
455
392
  }
456
393
  return stripDisallowedFullnames(users);
457
394
  };
458
395
 
459
- SocketPlugins.mentions.listGroups = function(socket, data, callback) {
396
+ SocketPlugins.mentions.listGroups = async function () {
460
397
  if (Mentions._settings.autofillGroups === 'off') {
461
- return callback(null, []);
398
+ return [];
462
399
  }
463
400
 
464
- Groups.getGroups('groups:visible:createtime', 0, -1, function(err, groups) {
465
- if (err) {
466
- return callback(err);
467
- }
468
- var noMentionGroups = getNoMentionGroups();
469
- groups = groups.filter(function(groupName) {
470
- return groupName && !noMentionGroups.includes(groupName);
471
- }).map(function(groupName) {
472
- return validator.escape(groupName);
473
- });
474
- callback(null, groups);
475
- });
401
+ const groups = await Groups.getGroups('groups:visible:createtime', 0, -1);
402
+ const noMentionGroups = getNoMentionGroups();
403
+ return groups.filter(g => g && !noMentionGroups.includes(g)).map(g => validator.escape(String(g)));
476
404
  };
477
405
 
478
406
  SocketPlugins.mentions.userSearch = async (socket, data) => {
479
- // Transparently pass request through to socket user.search handler
480
- const socketUser = require.main.require('./src/socket.io/user');
481
-
482
407
  // Search by username
483
408
  let { users } = await api.users.search(socket, data);
484
409
 
@@ -487,13 +412,13 @@ SocketPlugins.mentions.userSearch = async (socket, data) => {
487
412
  users = await stripDisallowedFullnames(users);
488
413
 
489
414
  // Search by fullname
490
- let { users: fullnameUsers } = await api.users.search(socket, {query: data.query, searchBy: 'fullname'});
415
+ let { users: fullnameUsers } = await api.users.search(socket, { query: data.query, searchBy: 'fullname' });
491
416
  // Hide results of users that do not allow their full name to be visible (prevents "enumeration attack")
492
417
  fullnameUsers = await filterDisallowedFullnames(fullnameUsers);
493
418
 
494
419
  // Merge results, filter duplicates (from username search, leave fullname results)
495
- users = users.filter(userObj =>
496
- fullnameUsers.filter(userObj2 => userObj.uid === userObj2.uid).length === 0
420
+ users = users.filter(
421
+ userObj => fullnameUsers.filter(userObj2 => userObj.uid === userObj2.uid).length === 0
497
422
  ).concat(fullnameUsers);
498
423
  }
499
424
 
@@ -505,10 +430,9 @@ SocketPlugins.mentions.userSearch = async (socket, data) => {
505
430
  const cid = Topics.getTopicField(data.composerObj.tid, 'cid');
506
431
  const filteredUids = await filterPrivilegedUids(users.map(userObj => userObj.uid), cid, data.composerObj.toPid);
507
432
 
508
- users = users.filter((userObj) => filteredUids.includes(userObj.uid));
433
+ users = users.filter(userObj => filteredUids.includes(userObj.uid));
509
434
  }
510
435
 
511
436
  return users;
512
437
  };
513
438
 
514
- module.exports = Mentions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-mentions",
3
- "version": "2.15.2",
3
+ "version": "3.0.0",
4
4
  "description": "NodeBB Plugin that allows users to mention other users by prepending an '@' sign to their username",
5
5
  "main": "library.js",
6
6
  "scripts": {
@@ -25,12 +25,15 @@
25
25
  "compatibility": "^1.15.0"
26
26
  },
27
27
  "dependencies": {
28
- "async": "^2",
29
28
  "html-entities": "^2.3.2",
29
+ "lodash": "4.17.21",
30
30
  "validator": "^13.0.0",
31
31
  "xregexp": "^5.1.0"
32
32
  },
33
33
  "devDependencies": {
34
- "mocha": "9.1.3"
34
+ "mocha": "9.1.3",
35
+ "eslint": "8.1.0",
36
+ "eslint-config-nodebb": "^0.0.3",
37
+ "eslint-plugin-import": "^2.24.2"
35
38
  }
36
39
  }
package/.jshintrc DELETED
@@ -1,86 +0,0 @@
1
- {
2
- // JSHint Default Configuration File (as on JSHint website)
3
- // See http://jshint.com/docs/ for more details
4
-
5
- "maxerr" : 50, // {int} Maximum error before stopping
6
-
7
- // Enforcing
8
- "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
9
- "camelcase" : false, // true: Identifiers must be in camelCase
10
- "curly" : true, // true: Require {} for every new block or scope
11
- "eqeqeq" : true, // true: Require triple equals (===) for comparison
12
- "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
13
- "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
14
- "indent" : 4, // {int} Number of spaces to use for indentation
15
- "latedef" : false, // true: Require variables/functions to be defined before being used
16
- "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()`
17
- "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
18
- "noempty" : true, // true: Prohibit use of empty blocks
19
- "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment)
20
- "plusplus" : false, // true: Prohibit use of `++` & `--`
21
- "quotmark" : false, // Quotation mark consistency:
22
- // false : do nothing (default)
23
- // true : ensure whatever is used is consistent
24
- // "single" : require single quotes
25
- // "double" : require double quotes
26
- "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
27
- "unused" : true, // true: Require all defined variables be used
28
- "strict" : true, // true: Requires all functions run in ES5 Strict Mode
29
- "trailing" : false, // true: Prohibit trailing whitespaces
30
- "maxparams" : false, // {int} Max number of formal params allowed per function
31
- "maxdepth" : false, // {int} Max depth of nested blocks (within functions)
32
- "maxstatements" : false, // {int} Max number statements per function
33
- "maxcomplexity" : false, // {int} Max cyclomatic complexity per function
34
- "maxlen" : false, // {int} Max number of characters per line
35
-
36
- // Relaxing
37
- "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
38
- "boss" : false, // true: Tolerate assignments where comparisons would be expected
39
- "debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
40
- "eqnull" : false, // true: Tolerate use of `== null`
41
- "es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
42
- "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
43
- "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
44
- // (ex: `for each`, multiple try/catch, function expression…)
45
- "evil" : false, // true: Tolerate use of `eval` and `new Function()`
46
- "expr" : false, // true: Tolerate `ExpressionStatement` as Programs
47
- "funcscope" : false, // true: Tolerate defining variables inside control statements"
48
- "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
49
- "iterator" : false, // true: Tolerate using the `__iterator__` property
50
- "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
51
- "laxbreak" : false, // true: Tolerate possibly unsafe line breakings
52
- "laxcomma" : false, // true: Tolerate comma-first style coding
53
- "loopfunc" : false, // true: Tolerate functions being defined in loops
54
- "multistr" : false, // true: Tolerate multi-line strings
55
- "proto" : false, // true: Tolerate using the `__proto__` property
56
- "scripturl" : false, // true: Tolerate script-targeted URLs
57
- "smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment
58
- "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
59
- "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
60
- "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
61
- "validthis" : false, // true: Tolerate using this in a non-constructor function
62
-
63
- // Environments
64
- "browser" : true, // Web Browser (window, document, etc)
65
- "couch" : false, // CouchDB
66
- "devel" : true, // Development/debugging (alert, confirm, etc)
67
- "dojo" : false, // Dojo Toolkit
68
- "jquery" : true, // jQuery
69
- "mootools" : false, // MooTools
70
- "node" : true, // Node.js
71
- "nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
72
- "prototypejs" : false, // Prototype and Scriptaculous
73
- "rhino" : false, // Rhino
74
- "worker" : false, // Web Workers
75
- "wsh" : false, // Windows Scripting Host
76
- "yui" : false, // Yahoo User Interface
77
-
78
- // Legacy
79
- "nomen" : false, // true: Prohibit dangling `_` in variables
80
- "onevar" : false, // true: Allow only one `var` statement per function
81
- "passfail" : false, // true: Stop on first error
82
- "white" : false, // true: Check against strict whitespace and indentation rules
83
-
84
- // Custom Globals
85
- "globals" : {} // additional predefined global variables
86
- }