nodebb-plugin-mentions 4.8.11 → 4.8.13

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/LICENSE CHANGED
@@ -1,8 +1,8 @@
1
- Copyright (c) 2013-2014, Julian Lam <julian@designcreateplay.com>
2
- All rights reserved.
3
-
4
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
-
6
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
1
+ Copyright (c) 2013-2014, Julian Lam <julian@designcreateplay.com>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+ Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
8
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,14 +1,14 @@
1
- # Username/Group Mentions
2
-
3
- This NodeBB plugin allows posters to reference (or *mention*) other users or groups on a NodeBB by simply
4
- precluding the `@` symbol before a username.
5
-
6
- A link is automatically added to the post.
7
-
8
- ## Installation
9
-
10
- This plugin is bundled with every NodeBB install. If not, you can install it via the Plugins page of the ACP.
11
-
12
- Alternatively,
13
-
1
+ # Username/Group Mentions
2
+
3
+ This NodeBB plugin allows posters to reference (or *mention*) other users or groups on a NodeBB by simply
4
+ precluding the `@` symbol before a username.
5
+
6
+ A link is automatically added to the post.
7
+
8
+ ## Installation
9
+
10
+ This plugin is bundled with every NodeBB install. If not, you can install it via the Plugins page of the ACP.
11
+
12
+ Alternatively,
13
+
14
14
  npm install nodebb-plugin-mentions
package/library.js CHANGED
@@ -10,7 +10,6 @@ const nconf = require.main.require('nconf');
10
10
  const winston = require.main.require('winston');
11
11
 
12
12
  const db = require.main.require('./src/database');
13
- const api = require.main.require('./src/api');
14
13
  const meta = require.main.require('./src/meta');
15
14
  const categories = require.main.require('./src/categories');
16
15
  const Topics = require.main.require('./src/topics');
@@ -27,6 +26,7 @@ const batch = require.main.require('./src/batch');
27
26
  const utils = require.main.require('./src/utils');
28
27
  const SocketPlugins = require.main.require('./src/socket.io/plugins');
29
28
  const translator = require.main.require('./src/translator');
29
+ const privileges = require.main.require('./src/privileges');
30
30
 
31
31
  const utility = require('./lib/utility');
32
32
 
@@ -40,10 +40,9 @@ const isLatinMention = /@[\w\d\-_.@]+$/;
40
40
 
41
41
  const Mentions = module.exports;
42
42
 
43
- Mentions._settings = {};
44
43
  Mentions._defaults = {
45
44
  disableFollowedTopics: 'off',
46
- autofillGroups: 'off',
45
+ autofillGroups: 'on',
47
46
  disableGroupMentions: '[]',
48
47
  overrideIgnores: 'off',
49
48
  display: '',
@@ -57,11 +56,13 @@ Mentions.init = async (data) => {
57
56
  const controllers = require('./controllers');
58
57
 
59
58
  routeHelpers.setupAdminPageRoute(data.router, '/admin/plugins/mentions', controllers.renderAdminPage);
60
-
61
- // Retrieve settings
62
- Object.assign(Mentions._settings, Mentions._defaults, await Meta.settings.get('mentions'));
63
59
  };
64
60
 
61
+ async function getSettings() {
62
+ const settings = await Meta.settings.get('mentions');
63
+ return { ...Mentions._defaults, ...settings };
64
+ }
65
+
65
66
  Mentions.addAdminNavigation = async (header) => {
66
67
  header.plugins.push({
67
68
  route: '/plugins/mentions',
@@ -71,10 +72,11 @@ Mentions.addAdminNavigation = async (header) => {
71
72
  return header;
72
73
  };
73
74
 
74
- function getNoMentionGroups() {
75
- let noMentionGroups = ['registered-users', 'verified-users', 'unverified-users', 'guests'];
75
+ async function getNoMentionGroups() {
76
+ let noMentionGroups = ['registered-users', 'verified-users', 'unverified-users', 'guests', 'banned-users'];
76
77
  try {
77
- noMentionGroups = noMentionGroups.concat(JSON.parse(Mentions._settings.disableGroupMentions));
78
+ const settings = await getSettings();
79
+ noMentionGroups = noMentionGroups.concat(JSON.parse(settings.disableGroupMentions));
78
80
  } catch (err) {
79
81
  winston.error(err);
80
82
  }
@@ -93,7 +95,7 @@ Mentions.notify = async function ({ post }) {
93
95
  return;
94
96
  }
95
97
 
96
- const noMentionGroups = getNoMentionGroups();
98
+ const noMentionGroups = await getNoMentionGroups();
97
99
  matches = _.uniq(
98
100
  matches
99
101
  .map(match => slugify(match.slice(1)))
@@ -129,11 +131,11 @@ Mentions.notify = async function ({ post }) {
129
131
  if ((!uidsToNotify && !groupsToNotify) || (!uidsToNotify.length && !groupsToNotify.length)) {
130
132
  return;
131
133
  }
132
-
134
+ const settings = await getSettings();
133
135
  const [topic, userData, topicFollowers] = await Promise.all([
134
136
  Topics.getTopicFields(post.tid, ['title', 'cid']),
135
137
  User.getUserFields(post.uid, ['username']),
136
- Mentions._settings.disableFollowedTopics === 'on' ? Topics.getFollowers(post.tid) : [],
138
+ settings.disableFollowedTopics === 'on' ? Topics.getFollowers(post.tid) : [],
137
139
  ]);
138
140
  const { displayname } = userData;
139
141
  const title = entitiesDecode(topic.title);
@@ -142,7 +144,7 @@ Mentions.notify = async function ({ post }) {
142
144
  uid => uid !== postOwner && !topicFollowers.includes(uid)
143
145
  );
144
146
 
145
- if (Mentions._settings.privilegedDirectReplies === 'on') {
147
+ if (settings.privilegedDirectReplies === 'on') {
146
148
  const toPid = await posts.getPostField(post.pid, 'toPid');
147
149
  uids = await filterPrivilegedUids(uids, post.cid, toPid);
148
150
  }
@@ -298,10 +300,10 @@ async function sendNotificationToUids(postData, uids, nidType, notificationText)
298
300
  if (!notification) {
299
301
  return;
300
302
  }
301
-
303
+ const settings = await getSettings();
302
304
  await batch.processArray(uids, async (uids) => {
303
305
  uids = await Privileges.topics.filterUids('read', postData.tid, uids);
304
- if (Mentions._settings.overrideIgnores !== 'on') {
306
+ if (settings.overrideIgnores !== 'on') {
305
307
  uids = await Topics.filterIgnoringUids(postData.tid, uids);
306
308
  }
307
309
  filteredUids.push(...uids);
@@ -423,6 +425,8 @@ Mentions.parseRaw = async (content, type = 'default') => {
423
425
  return content;
424
426
  }
425
427
 
428
+ const settings = await getSettings();
429
+
426
430
  matches = _.uniq(matches).map((match) => {
427
431
  /**
428
432
  * Javascript-flavour of regex does not support lookaround,
@@ -480,9 +484,10 @@ Mentions.parseRaw = async (content, type = 'default') => {
480
484
  }
481
485
  }));
482
486
 
487
+
483
488
  replacements = Array.from(replacements)
484
489
  .sort((a, b) => {
485
- return b.user.userslug.length - a.user.userslug.length;
490
+ return b.user && a.user ? b.user.userslug.length - a.user.userslug.length : 0;
486
491
  })
487
492
  .forEach(({ match, url, user, mentionType }) => {
488
493
  const regex = isLatinMention.test(match) ?
@@ -502,7 +507,7 @@ Mentions.parseRaw = async (content, type = 'default') => {
502
507
  const plain = match.slice(0, atIndex);
503
508
  match = match.slice(atIndex + 1);
504
509
  if (user && user.uid) {
505
- switch (Mentions._settings.display) {
510
+ switch (settings.display) {
506
511
  case 'fullname':
507
512
  match = user.displayname || match;
508
513
  break;
@@ -514,9 +519,9 @@ Mentions.parseRaw = async (content, type = 'default') => {
514
519
 
515
520
  let str;
516
521
  if (type === 'markdown') {
517
- str = `[${!Mentions._settings.display ? '@' : ''}${match}](${nconf.get('url')}${url})`;
522
+ str = `[${!settings.display ? '@' : ''}${match}](${nconf.get('url')}${url})`;
518
523
  } else {
519
- str = `<a class="plugin-mentions-${mentionType} plugin-mentions-a" href="${nconf.get('relative_path')}${url}" aria-label="Profile: ${match}">${!Mentions._settings.display ? '@' : ''}<bdi>${match}</bdi></a>`;
524
+ str = `<a class="plugin-mentions-${mentionType} plugin-mentions-a" href="${nconf.get('relative_path')}${url}" aria-label="Profile: ${match}">${!settings.display ? '@' : ''}<bdi>${match}</bdi></a>`;
520
525
  }
521
526
 
522
527
  return plain + str;
@@ -562,17 +567,18 @@ async function filterPrivilegedUids(uids, cid, toPid) {
562
567
  }
563
568
 
564
569
  async function filterDisallowedFullnames(users) {
570
+ if (!users.length) return [];
565
571
  const userSettings = await User.getMultipleUserSettings(users.map(user => user.uid));
566
- return users.filter((user, index) => userSettings[index].showfullname);
572
+ return users.filter((user, index) => userSettings[index] && userSettings[index].showfullname);
567
573
  }
568
574
 
569
575
  async function stripDisallowedFullnames(users) {
576
+ if (!users.length) return [];
570
577
  const userSettings = await User.getMultipleUserSettings(users.map(user => user.uid));
571
- return users.map((user, index) => {
572
- if (!userSettings[index].showfullname) {
578
+ users.forEach((user, index) => {
579
+ if (user && userSettings[index] && !userSettings[index].showfullname) {
573
580
  user.fullname = null;
574
581
  }
575
- return user;
576
582
  });
577
583
  }
578
584
 
@@ -587,39 +593,62 @@ SocketPlugins.mentions.getTopicUsers = async (socket, data) => {
587
593
  if (Meta.config.hideFullname) {
588
594
  return users;
589
595
  }
590
- return stripDisallowedFullnames(users);
596
+ await stripDisallowedFullnames(users);
597
+ return users;
591
598
  };
592
599
 
593
600
  SocketPlugins.mentions.listGroups = async function () {
594
- if (Mentions._settings.autofillGroups === 'off') {
601
+ const settings = await getSettings();
602
+ if (settings.autofillGroups === 'off') {
595
603
  return [];
596
604
  }
597
605
 
598
606
  const groups = await Groups.getGroups('groups:visible:createtime', 0, -1);
599
- const noMentionGroups = getNoMentionGroups();
600
- return groups.filter(g => g && !noMentionGroups.includes(g)).map(g => validator.escape(String(g)));
607
+ const noMentionGroups = await getNoMentionGroups();
608
+ const filteredGroups = groups.filter(g => g && !noMentionGroups.includes(g))
609
+ .map(g => validator.escape(String(g)));
610
+
611
+ return filteredGroups;
601
612
  };
602
613
 
603
614
  SocketPlugins.mentions.userSearch = async (socket, data) => {
604
- // Search by username
605
- let { users } = await api.users.search(socket, data);
615
+ const allowed = await privileges.global.can('search:users', socket.uid);
616
+ if (!allowed) {
617
+ throw new Error('[[error:no-privileges]]');
618
+ }
606
619
 
607
- if (!Meta.config.hideFullname) {
608
- // Strip fullnames of users that do not allow their full name to be visible
609
- users = await stripDisallowedFullnames(users);
620
+ const searchOpts = {
621
+ uid: socket.uid,
622
+ query: data.query,
623
+ sortBy: 'postcount',
624
+ hardCap: 1000,
625
+ paginate: true,
626
+ resultsPerPage: 100,
627
+ };
628
+
629
+ const [byUsername, byFullname, settings] = await Promise.all([
630
+ User.search({ ...searchOpts, searchBy: 'username' }),
631
+ !Meta.config.hideFullname ?
632
+ User.search({ ...searchOpts, searchBy: 'fullname' }) :
633
+ Promise.resolve({ users : [] }),
634
+ getSettings(),
635
+ ]);
610
636
 
611
- // Search by fullname
612
- let { users: fullnameUsers } = await api.users.search(socket, { query: data.query, searchBy: 'fullname' });
637
+ let { users } = byUsername;
638
+
639
+ const [fullnameUsers] = await Promise.all([
613
640
  // Hide results of users that do not allow their full name to be visible (prevents "enumeration attack")
614
- fullnameUsers = await filterDisallowedFullnames(fullnameUsers);
641
+ filterDisallowedFullnames(byFullname.users),
642
+ // Strip fullnames of users that do not allow their full name to be visible
643
+ stripDisallowedFullnames(users),
644
+ ]);
615
645
 
616
- // Merge results, filter duplicates (from username search, leave fullname results)
617
- users = users.filter(
618
- userObj => fullnameUsers.filter(userObj2 => userObj.uid === userObj2.uid).length === 0
619
- ).concat(fullnameUsers);
620
- }
646
+ // Merge results, filter duplicates (from username search, leave fullname results)
647
+ const fullnameUidSet = new Set(fullnameUsers.map(u => u.uid));
648
+ users = users.filter(u => !fullnameUidSet.has(u.uid)).concat(fullnameUsers);
649
+ users.sort((a, b) => b.postcount - a.postcount);
621
650
 
622
- if (Mentions._settings.privilegedDirectReplies === 'on') {
651
+ if (settings.privilegedDirectReplies === 'on') {
623
652
  if (data.composerObj) {
624
653
  const cid = Topics.getTopicField(data.composerObj.tid, 'cid');
625
654
  const filteredUids = await filterPrivilegedUids(users.map(userObj => userObj.uid), cid, data.composerObj.toPid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-mentions",
3
- "version": "4.8.11",
3
+ "version": "4.8.13",
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
  "repository": {
package/static/admin.js CHANGED
@@ -6,30 +6,12 @@ define('admin/plugins/mentions', ['settings', 'alerts'], function (Settings, ale
6
6
  ACP.init = function () {
7
7
  Settings.load('mentions', $('.mentions-settings'));
8
8
 
9
- $(window).on('action:admin.settingsLoaded', applyDefaults);
10
-
11
9
  $('#save').on('click', function () {
12
10
  Settings.save('mentions', $('.mentions-settings'), function () {
13
- alerts.alert({
14
- type: 'success',
15
- alert_id: 'mentions-saved',
16
- title: 'Settings Saved',
17
- message: 'Please reload your NodeBB to apply these settings',
18
- timeout: 5000,
19
- clickfn: function () {
20
- socket.emit('admin.reload');
21
- },
22
- });
11
+ alerts.success('Settings Saved');
23
12
  });
24
13
  });
25
14
  };
26
15
 
27
- function applyDefaults() {
28
- if (!ajaxify.data.settings || !ajaxify.data.settings.hasOwnProperty('autofillGroups')) {
29
- $('input#autofillGroups').parents('.mdl-switch').toggleClass('is-checked', false);
30
- $('input#autofillGroups').prop('checked', false);
31
- }
32
- }
33
-
34
16
  return ACP;
35
17
  });
@@ -8,6 +8,12 @@ $(document).ready(function () {
8
8
  let localUserList = [];
9
9
  let helpers;
10
10
 
11
+ function showAlert(type, message) {
12
+ require(['alerts'], function (alerts) {
13
+ alerts[type](message);
14
+ });
15
+ }
16
+
11
17
  $(window).on('composer:autocomplete:init chat:autocomplete:init', function (ev, data) {
12
18
  loadTopicUsers(data.element);
13
19
 
@@ -36,37 +42,30 @@ $(document).ready(function () {
36
42
  composerObj: composer.posts[uuid],
37
43
  }, function (err, users) {
38
44
  if (err) {
39
- require(['alerts'], function (alerts) {
40
- alerts.alert({
41
- id: 'mention-error',
42
- type: 'danger',
43
- message: err.message,
44
- timeout: 5000,
45
- });
46
- });
45
+ showAlert('error', err.message);
47
46
  return callback([]);
48
47
  }
49
48
  const termLowerCase = term.toLocaleLowerCase();
50
49
  const localMatches = localUserList.filter(
51
50
  u => u.username.toLocaleLowerCase().startsWith(termLowerCase)
52
51
  );
53
- const categoryMatches = categoryList.filter(c => c && c.handle && c.handle.startsWith(termLowerCase));
52
+ const categoryMatches = categoryList.filter(
53
+ c => c && c.handle && c.handle.startsWith(termLowerCase)
54
+ );
54
55
 
55
56
  // remove local matches from search results, add category matches
56
57
  users = users.filter(u => !localMatches.find(lu => lu.uid === u.uid));
57
- users = sortEntries(localMatches).concat(sortEntries([...users, ...categoryMatches]));
58
- // mentions = entriesToMentions(users, helpers);
58
+
59
+ users = sortEntries(localMatches).concat(users).concat(sortEntries(categoryMatches));
59
60
 
60
61
  // Add groups that start with the search term
61
- const groupMentions = groupList.filter(function (groupName) {
62
- return groupName.toLocaleLowerCase().startsWith(termLowerCase);
63
- }).sort(function (a, b) {
64
- return a.toLocaleLowerCase() > b.toLocaleLowerCase() ? 1 : -1;
65
- });
62
+ const groupMentions = groupList.filter(
63
+ group => group.name.toLocaleLowerCase().startsWith(termLowerCase) ||
64
+ group.slug.startsWith(termLowerCase)
65
+ ).sort((a, b) =>a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase() ? 1 : -1)
66
+ .map(group => group.name);
66
67
 
67
68
  // Add group mentions at the bottom of dropdown
68
- // mentions = mentions.concat(groupMentions);
69
-
70
69
  callback([...users, ...groupMentions]);
71
70
  });
72
71
  });
@@ -86,6 +85,12 @@ $(document).ready(function () {
86
85
  };
87
86
 
88
87
  data.strategies.push(strategy);
88
+ data.options = {
89
+ ...data.options,
90
+ ...{
91
+ maxCount: 100,
92
+ },
93
+ };
89
94
  });
90
95
 
91
96
  $(window).on('action:composer.loaded', function (ev, data) {
@@ -121,7 +126,7 @@ $(document).ready(function () {
121
126
  }
122
127
 
123
128
  function loadTopicUsers(element) {
124
- require(['composer', 'alerts'], function (composer, alerts) {
129
+ require(['composer'], function (composer) {
125
130
  function findTid() {
126
131
  const composerEl = element.parents('.composer').get(0);
127
132
  if (composerEl) {
@@ -146,7 +151,7 @@ $(document).ready(function () {
146
151
  tid: tid,
147
152
  }, function (err, users) {
148
153
  if (err) {
149
- return alerts.error(err);
154
+ return showAlert('error', err);
150
155
  }
151
156
  localUserList = users;
152
157
  });
@@ -154,14 +159,12 @@ $(document).ready(function () {
154
159
  }
155
160
 
156
161
  function loadGroupList() {
157
- socket.emit('plugins.mentions.listGroups', function (err, groupNames) {
162
+ socket.emit('plugins.mentions.listGroups', async function (err, groupNames) {
158
163
  if (err) {
159
- require(['alerts'], function (alerts) {
160
- alerts.error(err);
161
- });
162
- return;
164
+ return showAlert('error', err.message);
163
165
  }
164
- groupList = groupNames;
166
+ const s = await app.require('slugify');
167
+ groupList = groupNames.map(name => ({ name, slug: s(name) }));
165
168
  });
166
169
  }
167
170