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.
- package/.eslintrc +3 -0
- package/LICENSE +7 -7
- package/README.md +13 -13
- package/controllers.js +5 -9
- package/languages/ar/notifications.json +2 -2
- package/languages/bg/notifications.json +2 -2
- package/languages/bn/notifications.json +2 -2
- package/languages/da/notifications.json +2 -2
- package/languages/de/notifications.json +5 -5
- package/languages/el/notifications.json +2 -2
- package/languages/en@pirate/notifications.json +4 -4
- package/languages/en_GB/mentions.json +3 -3
- package/languages/en_GB/notifications.json +6 -6
- package/languages/en_US/notifications.json +4 -4
- package/languages/es/notifications.json +3 -3
- package/languages/et/notifications.json +2 -2
- package/languages/fa_IR/notifications.json +4 -4
- package/languages/fi/notifications.json +2 -2
- package/languages/fr/notifications.json +5 -5
- package/languages/gl/notifications.json +2 -2
- package/languages/he/notifications.json +2 -2
- package/languages/id/notifications.json +2 -2
- package/languages/it/notifications.json +3 -3
- package/languages/ja/notifications.json +4 -4
- package/languages/jbo/notifications.json +4 -4
- package/languages/ko/notifications.json +2 -2
- package/languages/lt/notifications.json +2 -2
- package/languages/ms/notifications.json +2 -2
- package/languages/nb/notifications.json +2 -2
- package/languages/nl/notifications.json +2 -2
- package/languages/pl/mentions.json +3 -3
- package/languages/pl/notifications.json +6 -6
- package/languages/pt-PT/notifications.json +6 -6
- package/languages/pt_BR/notifications.json +3 -3
- package/languages/ro/notifications.json +2 -2
- package/languages/rw/notifications.json +2 -2
- package/languages/sl/notifications.json +2 -2
- package/languages/sr/notifications.json +2 -2
- package/languages/sv/notifications.json +2 -2
- package/languages/tr/notifications.json +6 -6
- package/languages/vi/notifications.json +2 -2
- package/languages/zh-CN/mentions.json +3 -3
- package/languages/zh-CN/notifications.json +5 -5
- package/languages/zh_TW/notifications.json +2 -2
- package/lib/utility.js +6 -4
- package/library.js +200 -268
- package/package.json +8 -5
- package/plugin.json +30 -26
- package/static/admin.js +35 -35
- package/templates/admin/plugins/mentions.tpl +64 -64
- package/upgrades/mentions_delete_mentions_set_zset.js +21 -0
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
});
|
|
91
|
+
const [uidsToNotify, groupsToNotify] = await Promise.all([
|
|
92
|
+
getUidsToNotify(matches),
|
|
93
|
+
getGroupsToNotify(matches),
|
|
94
|
+
]);
|
|
93
95
|
|
|
94
|
-
if (!
|
|
96
|
+
if (!uidsToNotify.length && !groupsToNotify.length) {
|
|
95
97
|
return;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
106
|
+
const title = entitiesDecode(topic.title);
|
|
107
|
+
const titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
|
113
108
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
312
|
-
splitContent.forEach(
|
|
313
|
-
if ((i
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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(
|
|
299
|
+
splitContent = splitContent.map((c, i) => {
|
|
353
300
|
// *Might* not be needed anymore? Check pls...
|
|
354
|
-
if (skip || (i
|
|
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,
|
|
305
|
+
return c.replace(regex, (match) => {
|
|
359
306
|
// Again, cleaning up lookaround leftover bits
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
396
|
+
SocketPlugins.mentions.listGroups = async function () {
|
|
452
397
|
if (Mentions._settings.autofillGroups === 'off') {
|
|
453
|
-
return
|
|
398
|
+
return [];
|
|
454
399
|
}
|
|
455
400
|
|
|
456
|
-
Groups.getGroups('groups:visible:createtime', 0, -1
|
|
457
|
-
|
|
458
|
-
|
|
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(
|
|
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(
|
|
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;
|