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 +3 -0
- package/controllers.js +10 -14
- package/lib/utility.js +18 -16
- package/library.js +191 -267
- package/package.json +6 -3
- package/.jshintrc +0 -86
package/.eslintrc
ADDED
package/controllers.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Controllers.renderAdminPage = function (req, res
|
|
8
|
-
groups.getGroupsFromSet('groups:visible:createtime', 0, -1
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
Utility
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
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
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
141
|
-
|
|
96
|
+
if (!uidsToNotify.length && !groupsToNotify.length) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
142
99
|
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
uids = await filterPrivilegedUids(uids, data.post.cid, toPid);
|
|
150
|
-
}
|
|
106
|
+
const title = entitiesDecode(topic.title);
|
|
107
|
+
const titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
|
151
108
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
320
|
-
splitContent.forEach(
|
|
321
|
-
if ((i
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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(
|
|
299
|
+
splitContent = splitContent.map((c, i) => {
|
|
361
300
|
// *Might* not be needed anymore? Check pls...
|
|
362
|
-
if (skip || (i
|
|
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,
|
|
305
|
+
return c.replace(regex, (match) => {
|
|
367
306
|
// Again, cleaning up lookaround leftover bits
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
396
|
+
SocketPlugins.mentions.listGroups = async function () {
|
|
460
397
|
if (Mentions._settings.autofillGroups === 'off') {
|
|
461
|
-
return
|
|
398
|
+
return [];
|
|
462
399
|
}
|
|
463
400
|
|
|
464
|
-
Groups.getGroups('groups:visible:createtime', 0, -1
|
|
465
|
-
|
|
466
|
-
|
|
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(
|
|
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(
|
|
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": "
|
|
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
|
-
}
|