nodebb-plugin-mentions 2.14.1 → 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.
Files changed (52) hide show
  1. package/.eslintrc +3 -0
  2. package/LICENSE +7 -7
  3. package/README.md +13 -13
  4. package/controllers.js +5 -9
  5. package/languages/ar/notifications.json +2 -2
  6. package/languages/bg/notifications.json +2 -2
  7. package/languages/bn/notifications.json +2 -2
  8. package/languages/da/notifications.json +2 -2
  9. package/languages/de/notifications.json +5 -5
  10. package/languages/el/notifications.json +2 -2
  11. package/languages/en@pirate/notifications.json +4 -4
  12. package/languages/en_GB/mentions.json +3 -3
  13. package/languages/en_GB/notifications.json +6 -6
  14. package/languages/en_US/notifications.json +4 -4
  15. package/languages/es/notifications.json +3 -3
  16. package/languages/et/notifications.json +2 -2
  17. package/languages/fa_IR/notifications.json +4 -4
  18. package/languages/fi/notifications.json +2 -2
  19. package/languages/fr/notifications.json +5 -5
  20. package/languages/gl/notifications.json +2 -2
  21. package/languages/he/notifications.json +2 -2
  22. package/languages/id/notifications.json +2 -2
  23. package/languages/it/notifications.json +3 -3
  24. package/languages/ja/notifications.json +4 -4
  25. package/languages/jbo/notifications.json +4 -4
  26. package/languages/ko/notifications.json +2 -2
  27. package/languages/lt/notifications.json +2 -2
  28. package/languages/ms/notifications.json +2 -2
  29. package/languages/nb/notifications.json +2 -2
  30. package/languages/nl/notifications.json +2 -2
  31. package/languages/pl/mentions.json +3 -3
  32. package/languages/pl/notifications.json +6 -6
  33. package/languages/pt-PT/notifications.json +6 -6
  34. package/languages/pt_BR/notifications.json +3 -3
  35. package/languages/ro/notifications.json +2 -2
  36. package/languages/rw/notifications.json +2 -2
  37. package/languages/sl/notifications.json +2 -2
  38. package/languages/sr/notifications.json +2 -2
  39. package/languages/sv/notifications.json +2 -2
  40. package/languages/tr/notifications.json +6 -6
  41. package/languages/vi/notifications.json +2 -2
  42. package/languages/zh-CN/mentions.json +3 -3
  43. package/languages/zh-CN/notifications.json +5 -5
  44. package/languages/zh_TW/notifications.json +2 -2
  45. package/lib/utility.js +6 -4
  46. package/library.js +200 -268
  47. package/package.json +8 -5
  48. package/plugin.json +30 -26
  49. package/static/admin.js +35 -35
  50. package/templates/admin/plugins/mentions.tpl +64 -64
  51. package/upgrades/mentions_delete_mentions_set_zset.js +21 -0
  52. package/.jshintrc +0 -86
package/library.js CHANGED
@@ -1,55 +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 Entities = require('html-entities').XmlEntities;
33
- var entities = new Entities();
34
-
35
- var Mentions = {
36
- _settings: {},
37
- _defaults: {
38
- disableFollowedTopics: 'off',
39
- autofillGroups: 'off',
40
- disableGroupMentions: '[]',
41
- overrideIgnores: 'off',
42
- display: '',
43
- }
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: '',
44
42
  };
43
+
45
44
  SocketPlugins.mentions = {};
46
45
 
47
46
  Mentions.init = async (data) => {
48
- var hostMiddleware = require.main.require('./src/middleware');
49
- 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');
50
50
 
51
- data.router.get('/admin/plugins/mentions', hostMiddleware.admin.buildHeader, controllers.renderAdminPage);
52
- data.router.get('/api/admin/plugins/mentions', controllers.renderAdminPage);
51
+ routeHelpers.setupAdminPageRoute(data.router, '/admin/plugins/mentions', hostMiddleware, [], controllers.renderAdminPage);
53
52
 
54
53
  // Retrieve settings
55
54
  Object.assign(Mentions._settings, Mentions._defaults, await Meta.settings.get('mentions'));
@@ -58,14 +57,14 @@ Mentions.init = async (data) => {
58
57
  Mentions.addAdminNavigation = async (header) => {
59
58
  header.plugins.push({
60
59
  route: '/plugins/mentions',
61
- name: 'Mentions'
60
+ name: 'Mentions',
62
61
  });
63
62
 
64
63
  return header;
65
64
  };
66
65
 
67
66
  function getNoMentionGroups() {
68
- var noMentionGroups = ['registered-users', 'guests'];
67
+ let noMentionGroups = ['registered-users', 'verified-users', 'unverified-users', 'guests'];
69
68
  try {
70
69
  noMentionGroups = noMentionGroups.concat(JSON.parse(Mentions._settings.disableGroupMentions));
71
70
  } catch (err) {
@@ -74,105 +73,114 @@ function getNoMentionGroups() {
74
73
  return noMentionGroups;
75
74
  }
76
75
 
77
- Mentions.notify = function(data) {
78
- var postData = data.post;
79
- var cleanedContent = Mentions.clean(postData.content, true, true, true);
80
- var matches = cleanedContent.match(regex);
81
-
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);
82
81
  if (!matches) {
83
82
  return;
84
83
  }
85
84
 
86
- var noMentionGroups = getNoMentionGroups();
85
+ const noMentionGroups = getNoMentionGroups();
86
+ matches = _.uniq(matches.map(match => slugify(match))).filter(match => match && !noMentionGroups.includes(match));
87
+ if (!matches.length) {
88
+ return;
89
+ }
87
90
 
88
- matches = matches.map(function(match) {
89
- return slugify(match);
90
- }).filter(function(match, index, array) {
91
- return match && array.indexOf(match) === index && noMentionGroups.indexOf(match) === -1;
92
- });
91
+ const [uidsToNotify, groupsToNotify] = await Promise.all([
92
+ getUidsToNotify(matches),
93
+ getGroupsToNotify(matches),
94
+ ]);
93
95
 
94
- if (!matches.length) {
96
+ if (!uidsToNotify.length && !groupsToNotify.length) {
95
97
  return;
96
98
  }
97
99
 
98
- async.parallel({
99
- userRecipients: function(next) {
100
- async.filter(matches, User.existsBySlug, next);
101
- },
102
- groupRecipients: function(next) {
103
- async.filter(matches, Groups.existsBySlug, next);
104
- }
105
- }, function(err, results) {
106
- if (err) {
107
- return;
108
- }
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
+ ]);
109
105
 
110
- if (!results.userRecipients.length && !results.groupRecipients.length) {
111
- return;
112
- }
106
+ const title = entitiesDecode(topic.title);
107
+ const titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
113
108
 
114
- async.parallel({
115
- topic: function(next) {
116
- Topics.getTopicFields(postData.tid, ['title', 'cid'], next);
117
- },
118
- author: function(next) {
119
- User.getUserField(postData.uid, 'username', next);
120
- },
121
- uids: function(next) {
122
- async.map(results.userRecipients, function(slug, next) {
123
- User.getUidByUserslug(slug, next);
124
- }, next);
125
- },
126
- groupData: function(next) {
127
- getGroupMemberUids(results.groupRecipients, next);
128
- },
129
- topicFollowers: function(next) {
130
- if (Mentions._settings.disableFollowedTopics === 'on') {
131
- Topics.getFollowers(postData.tid, next);
132
- } else {
133
- next(null, []);
134
- }
135
- }
136
- }, async (err, results) => {
137
- if (err) {
138
- return;
139
- }
109
+ let uids = uidsToNotify.filter(
110
+ uid => parseInt(uid, 10) !== postOwner && !topicFollowers.includes(uid)
111
+ );
140
112
 
141
- var title = entities.decode(results.topic.title);
142
- var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
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
+ }
143
117
 
144
- var uids = results.uids.filter(function(uid, index, array) {
145
- return array.indexOf(uid) === index && parseInt(uid, 10) !== parseInt(postData.uid, 10) && !results.topicFollowers.includes(uid.toString());
146
- });
118
+ const groupMemberUids = {};
119
+ groupsToNotify.forEach((groupData) => {
120
+ groupData.members = groupData.members.filter((uid) => {
121
+ if (!uid || groupMemberUids[uid]) {
122
+ return false;
123
+ }
124
+ groupMemberUids[uid] = 1;
125
+ return !uids.includes(uid) &&
126
+ parseInt(uid, 10) !== postOwner &&
127
+ !topicFollowers.includes(uid);
128
+ });
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
+ }
147
136
 
148
- if (Mentions._settings.privilegedDirectReplies === 'on') {
149
- const toPid = await posts.getPostField(data.post.pid, 'toPid');
150
- uids = await filterPrivilegedUids(uids, data.post.cid, toPid);
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);
151
145
  }
146
+ }
147
+ }
148
+ };
152
149
 
153
- var groupMemberUids = {};
154
- results.groupData.groupNames.forEach(function(groupName, index) {
155
- results.groupData.groupMembers[index] = results.groupData.groupMembers[index].filter(function(uid) {
156
- if (!uid || groupMemberUids[uid]) {
157
- return false;
158
- }
159
- groupMemberUids[uid] = 1;
160
- return !uids.includes(uid) &&
161
- parseInt(uid, 10) !== parseInt(postData.uid, 10) &&
162
- !results.topicFollowers.includes(uid.toString());
163
- });
164
- });
150
+ async function getUidsToNotify(matches) {
151
+ const uids = await db.sortedSetScores('userslug:uid', matches);
152
+ return _.uniq(uids.filter(Boolean).map(String));
153
+ }
165
154
 
166
- sendNotificationToUids(postData, uids, 'user', '[[notifications:user_mentioned_you_in, ' + results.author + ', ' + titleEscaped + ']]');
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
+ }
167
171
 
168
- results.groupData.groupNames.forEach(function(groupName, index) {
169
- var memberUids = results.groupData.groupMembers[index];
170
- sendNotificationToUids(postData, memberUids, groupName, '[[notifications:user_mentioned_group_in, ' + results.author + ', ' + groupName + ', ' + titleEscaped + ']]');
171
- });
172
- });
173
- });
172
+ Mentions.actionPostPurge = async (hookData) => {
173
+ await db.deleteAll([
174
+ `mentions:pid:${hookData.postData.pid}:uids`,
175
+ `mentions:pid:${hookData.postData.pid}:groups`,
176
+ ]);
174
177
  };
175
178
 
179
+ async function filterUidsAlreadyMentioned(uids, pid) {
180
+ const isMember = await db.isSetMembers(`mentions:pid:${pid}:uids`, uids);
181
+ return uids.filter((uid, index) => !isMember[index]);
182
+ }
183
+
176
184
  Mentions.addFilters = async (data) => {
177
185
  data.regularFilters.push({ name: '[[notifications:mentions]]', filter: 'mention' });
178
186
  return data;
@@ -190,109 +198,47 @@ Mentions.addFields = async (data) => {
190
198
  return data;
191
199
  };
192
200
 
193
- function sendNotificationToUids(postData, uids, nidType, notificationText) {
201
+ async function sendNotificationToUids(postData, uids, nidType, notificationText) {
194
202
  if (!uids.length) {
195
203
  return;
196
204
  }
197
205
 
198
- var filteredUids = [];
199
- var notification;
200
- async.waterfall([
201
- function (next) {
202
- createNotification(postData, nidType, notificationText, next);
203
- },
204
- function (_notification, next) {
205
- notification = _notification;
206
- if (!notification) {
207
- return next();
208
- }
209
-
210
- batch.processArray(uids, function (uids, next) {
211
- async.waterfall([
212
- function(next) {
213
- Privileges.topics.filterUids('read', postData.tid, uids, next);
214
- },
215
- function(_uids, next) {
216
- if (Mentions._settings.overrideIgnores === 'on') {
217
- return setImmediate(next, null, _uids);
218
- }
219
-
220
- Topics.filterIgnoringUids(postData.tid, _uids, next);
221
- },
222
- function (_uids, next) {
223
- // Filter out uids that have already been notified for this pid
224
- db.isSortedSetMembers('mentions:sent:' + postData.pid, _uids, function (err, exists) {
225
- next(err, _uids.filter((uid, idx) => !exists[idx]))
226
- });
227
- },
228
- function(_uids, next) {
229
- if (!_uids.length) {
230
- return next();
231
- }
232
-
233
- filteredUids = filteredUids.concat(_uids);
234
-
235
- next();
236
- }
237
- ], next);
238
- }, {
239
- interval: 1000,
240
- batch: 500,
241
- }, next);
242
- },
243
- ], function (err) {
244
- if (err) {
245
- return winston.error(err);
246
- }
206
+ const filteredUids = [];
207
+ const notification = await createNotification(postData, nidType, notificationText);
208
+ if (!notification) {
209
+ return;
210
+ }
247
211
 
248
- if (notification && filteredUids.length) {
249
- plugins.hooks.fire('action:mentions.notify', { notification, uids: filteredUids });
250
- Notifications.push(notification, filteredUids, function () {
251
- const dates = filteredUids.map(() => Date.now());
252
- db.sortedSetAdd('mentions:sent:' + postData.pid, dates, filteredUids);
253
- });
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);
254
216
  }
217
+ filteredUids.push(...uids);
218
+ }, {
219
+ interval: 1000,
220
+ batch: 500,
255
221
  });
256
- }
257
222
 
258
- function createNotification(postData, nidType, notificationText, callback) {
259
- Topics.getTopicField(postData.tid, 'title', function (err, title) {
260
- if (err) {
261
- return callback(err);
262
- }
263
- if (title) {
264
- title = utils.decodeHTMLEntities(title);
265
- }
266
- Notifications.create({
267
- type: 'mention',
268
- bodyShort: notificationText,
269
- bodyLong: postData.content,
270
- nid: 'tid:' + postData.tid + ':pid:' + postData.pid + ':uid:' + postData.uid + ':' + nidType,
271
- pid: postData.pid,
272
- tid: postData.tid,
273
- from: postData.uid,
274
- path: '/post/' + postData.pid,
275
- topicTitle: title,
276
- importance: 6,
277
- }, callback);
278
- });
223
+ if (notification && filteredUids.length) {
224
+ plugins.hooks.fire('action:mentions.notify', { notification, uids: filteredUids });
225
+ Notifications.push(notification, filteredUids);
226
+ }
279
227
  }
280
228
 
281
- function getGroupMemberUids(groupRecipients, callback) {
282
- async.map(groupRecipients, function(slug, next) {
283
- Groups.getGroupNameByGroupSlug(slug, next);
284
- }, function(err, groupNames) {
285
- if (err) {
286
- return callback(err);
287
- }
288
- async.map(groupNames, function(groupName, next) {
289
- Groups.getMembers(groupName, 0, -1, next);
290
- }, function(err, groupMembers) {
291
- if (err) {
292
- return callback(err);
293
- }
294
- callback(null, {groupNames: groupNames, groupMembers: groupMembers});
295
- });
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,
296
242
  });
297
243
  }
298
244
 
@@ -306,11 +252,15 @@ Mentions.parsePost = async (data) => {
306
252
  return data;
307
253
  };
308
254
 
255
+ function removePunctuationSuffix(string) {
256
+ return string.replace(/[!?.]*$/, '');
257
+ }
258
+
309
259
  Mentions.parseRaw = async (content) => {
310
260
  let splitContent = utility.split(content, false, false, true);
311
- var matches = [];
312
- splitContent.forEach(function(cleanedContent, i) {
313
- if ((i & 1) === 0) {
261
+ let matches = [];
262
+ splitContent.forEach((cleanedContent, i) => {
263
+ if ((i % 2) === 0) {
314
264
  matches = matches.concat(cleanedContent.match(regex) || []);
315
265
  }
316
266
  });
@@ -319,21 +269,18 @@ Mentions.parseRaw = async (content) => {
319
269
  return content;
320
270
  }
321
271
 
322
- matches = matches.filter(function(cur, idx) {
323
- // Eliminate duplicates
324
- return idx === matches.indexOf(cur);
325
- }).map(function(match) {
272
+ matches = _.uniq(matches).map((match) => {
326
273
  /**
327
274
  * Javascript-favour of regex does not support lookaround,
328
275
  * so need to clean up the cruft by discarding everthing
329
276
  * before the @
330
277
  */
331
- var atIndex = match.indexOf('@');
278
+ const atIndex = match.indexOf('@');
332
279
  return atIndex !== 0 ? match.slice(atIndex) : match;
333
280
  });
334
281
 
335
282
  await Promise.all(matches.map(async (match) => {
336
- var slug = slugify(match.slice(1));
283
+ const slug = slugify(match.slice(1));
337
284
  match = removePunctuationSuffix(match);
338
285
 
339
286
  const uid = await User.getUidByUserslug(slug);
@@ -343,22 +290,22 @@ Mentions.parseRaw = async (content) => {
343
290
  });
344
291
 
345
292
  if (results.user.uid || results.groupExists) {
346
- var regex = isLatinMention.test(match)
347
- ? new RegExp('(?:^|\\s|\>|;)' + match + '\\b', 'g')
348
- : new RegExp('(?:^|\\s|\>|;)' + match, 'g');
293
+ const regex = isLatinMention.test(match) ?
294
+ new RegExp(`(?:^|\\s|\>|;)${match}\\b`, 'g') :
295
+ new RegExp(`(?:^|\\s|\>|;)${match}`, 'g');
349
296
 
350
297
  let skip = false;
351
298
 
352
- splitContent = splitContent.map(function(c, i) {
299
+ splitContent = splitContent.map((c, i) => {
353
300
  // *Might* not be needed anymore? Check pls...
354
- if (skip || (i & 1) === 1) {
301
+ if (skip || (i % 2) === 1) {
355
302
  skip = c === '<code>'; // if code block detected, skip the content inside of it
356
303
  return c;
357
304
  }
358
- return c.replace(regex, function(match) {
305
+ return c.replace(regex, (match) => {
359
306
  // Again, cleaning up lookaround leftover bits
360
- var atIndex = match.indexOf('@');
361
- var plain = match.slice(0, atIndex);
307
+ const atIndex = match.indexOf('@');
308
+ const plain = match.slice(0, atIndex);
362
309
  match = match.slice(atIndex);
363
310
  if (results.user.uid) {
364
311
  switch (Mentions._settings.display) {
@@ -371,9 +318,9 @@ Mentions.parseRaw = async (content) => {
371
318
  }
372
319
  }
373
320
 
374
- var str = results.user.uid
375
- ? '<a class="plugin-mentions-user plugin-mentions-a" href="' + nconf.get('url') + '/uid/' + results.user.uid + '">' + match + '</a>'
376
- : '<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>`;
377
324
 
378
325
  return plain + str;
379
326
  });
@@ -384,19 +331,17 @@ Mentions.parseRaw = async (content) => {
384
331
  return splitContent.join('');
385
332
  };
386
333
 
387
- Mentions.clean = function(input, isMarkdown, stripBlockquote, stripCode) {
388
- var split = utility.split(input, isMarkdown, stripBlockquote, stripCode);
389
- split = split.filter(function(e, i) {
390
- // only keep non-code/non-blockquote
391
- return (i & 1) === 0;
392
- });
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);
393
338
  return split.join('');
394
339
  };
395
340
 
396
341
  /*
397
342
  Local utility methods
398
343
  */
399
- async function filterPrivilegedUids (uids, cid, toPid) {
344
+ async function filterPrivilegedUids(uids, cid, toPid) {
400
345
  let toPidUid;
401
346
  if (toPid) {
402
347
  toPidUid = await posts.getPostField(toPid, 'uid');
@@ -420,12 +365,12 @@ async function filterPrivilegedUids (uids, cid, toPid) {
420
365
  return uids.filter(Boolean);
421
366
  }
422
367
 
423
- async function filterDisallowedFullnames (users) {
368
+ async function filterDisallowedFullnames(users) {
424
369
  const userSettings = await User.getMultipleUserSettings(users.map(user => user.uid));
425
370
  return users.filter((user, index) => userSettings[index].showfullname);
426
371
  }
427
372
 
428
- async function stripDisallowedFullnames (users) {
373
+ async function stripDisallowedFullnames(users) {
429
374
  const userSettings = await User.getMultipleUserSettings(users.map(user => user.uid));
430
375
  return users.map((user, index) => {
431
376
  if (!userSettings[index].showfullname) {
@@ -441,36 +386,24 @@ async function stripDisallowedFullnames (users) {
441
386
 
442
387
  SocketPlugins.mentions.getTopicUsers = async (socket, data) => {
443
388
  const uids = await Topics.getUids(data.tid);
444
- const users = await User.getUsers(uids);
389
+ const users = await User.getUsers(uids);
445
390
  if (Meta.config.hideFullname) {
446
391
  return users;
447
392
  }
448
393
  return stripDisallowedFullnames(users);
449
394
  };
450
395
 
451
- SocketPlugins.mentions.listGroups = function(socket, data, callback) {
396
+ SocketPlugins.mentions.listGroups = async function () {
452
397
  if (Mentions._settings.autofillGroups === 'off') {
453
- return callback(null, []);
398
+ return [];
454
399
  }
455
400
 
456
- Groups.getGroups('groups:visible:createtime', 0, -1, function(err, groups) {
457
- if (err) {
458
- return callback(err);
459
- }
460
- var noMentionGroups = getNoMentionGroups();
461
- groups = groups.filter(function(groupName) {
462
- return groupName && !noMentionGroups.includes(groupName);
463
- }).map(function(groupName) {
464
- return validator.escape(groupName);
465
- });
466
- callback(null, groups);
467
- });
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)));
468
404
  };
469
405
 
470
406
  SocketPlugins.mentions.userSearch = async (socket, data) => {
471
- // Transparently pass request through to socket user.search handler
472
- const socketUser = require.main.require('./src/socket.io/user');
473
-
474
407
  // Search by username
475
408
  let { users } = await api.users.search(socket, data);
476
409
 
@@ -479,13 +412,13 @@ SocketPlugins.mentions.userSearch = async (socket, data) => {
479
412
  users = await stripDisallowedFullnames(users);
480
413
 
481
414
  // Search by fullname
482
- 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' });
483
416
  // Hide results of users that do not allow their full name to be visible (prevents "enumeration attack")
484
417
  fullnameUsers = await filterDisallowedFullnames(fullnameUsers);
485
418
 
486
419
  // Merge results, filter duplicates (from username search, leave fullname results)
487
- users = users.filter(userObj =>
488
- fullnameUsers.filter(userObj2 => userObj.uid === userObj2.uid).length === 0
420
+ users = users.filter(
421
+ userObj => fullnameUsers.filter(userObj2 => userObj.uid === userObj2.uid).length === 0
489
422
  ).concat(fullnameUsers);
490
423
  }
491
424
 
@@ -497,10 +430,9 @@ SocketPlugins.mentions.userSearch = async (socket, data) => {
497
430
  const cid = Topics.getTopicField(data.composerObj.tid, 'cid');
498
431
  const filteredUids = await filterPrivilegedUids(users.map(userObj => userObj.uid), cid, data.composerObj.toPid);
499
432
 
500
- users = users.filter((userObj) => filteredUids.includes(userObj.uid));
433
+ users = users.filter(userObj => filteredUids.includes(userObj.uid));
501
434
  }
502
435
 
503
436
  return users;
504
437
  };
505
438
 
506
- module.exports = Mentions;