@waline/vercel 1.26.3 → 1.26.4
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/dist/404.html +39 -0
- package/dist/500.html +275 -0
- package/dist/index.js +58501 -0
- package/dist/package.json +54 -0
- package/dist/src/config/adapter.js +170 -0
- package/dist/src/config/config.js +134 -0
- package/dist/src/config/extend.js +38 -0
- package/dist/src/config/middleware.js +66 -0
- package/dist/src/config/router.js +1 -0
- package/dist/src/controller/article.js +91 -0
- package/dist/src/controller/comment.js +758 -0
- package/dist/src/controller/db.js +71 -0
- package/dist/src/controller/index.js +36 -0
- package/dist/src/controller/oauth.js +136 -0
- package/dist/src/controller/rest.js +60 -0
- package/dist/src/controller/token/2fa.js +66 -0
- package/dist/src/controller/token.js +75 -0
- package/dist/src/controller/user/password.js +52 -0
- package/dist/src/controller/user.js +289 -0
- package/dist/src/controller/verification.js +35 -0
- package/dist/src/extend/controller.js +25 -0
- package/dist/src/extend/think.js +84 -0
- package/dist/src/locales/en.json +19 -0
- package/dist/src/locales/index.js +12 -0
- package/dist/src/locales/zh-CN.json +19 -0
- package/dist/src/locales/zh-TW.json +19 -0
- package/dist/src/logic/article.js +27 -0
- package/dist/src/logic/base.js +164 -0
- package/dist/src/logic/comment.js +317 -0
- package/dist/src/logic/db.js +81 -0
- package/dist/src/logic/oauth.js +10 -0
- package/dist/src/logic/token/2fa.js +28 -0
- package/dist/src/logic/token.js +53 -0
- package/dist/src/logic/user/password.js +11 -0
- package/dist/src/logic/user.js +117 -0
- package/dist/src/middleware/dashboard.js +23 -0
- package/dist/src/middleware/version.js +6 -0
- package/dist/src/service/akismet.js +41 -0
- package/dist/src/service/avatar.js +35 -0
- package/dist/src/service/markdown/highlight.js +32 -0
- package/dist/src/service/markdown/index.js +63 -0
- package/dist/src/service/markdown/katex.js +49 -0
- package/dist/src/service/markdown/mathCommon.js +156 -0
- package/dist/src/service/markdown/mathjax.js +78 -0
- package/dist/src/service/markdown/utils.js +11 -0
- package/dist/src/service/markdown/xss.js +44 -0
- package/dist/src/service/notify.js +537 -0
- package/dist/src/service/storage/base.js +31 -0
- package/dist/src/service/storage/cloudbase.js +221 -0
- package/dist/src/service/storage/deta.js +307 -0
- package/dist/src/service/storage/github.js +377 -0
- package/dist/src/service/storage/leancloud.js +430 -0
- package/dist/src/service/storage/mongodb.js +179 -0
- package/dist/src/service/storage/mysql.js +123 -0
- package/dist/src/service/storage/postgresql.js +84 -0
- package/dist/src/service/storage/sqlite.js +11 -0
- package/dist/src/service/storage/tidb.js +3 -0
- package/package.json +1 -1
- package/src/controller/comment.js +1 -2
- package/src/extend/think.js +19 -0
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
const parser = require('ua-parser-js');
|
|
2
|
+
const BaseRest = require('./rest');
|
|
3
|
+
const akismet = require('../service/akismet');
|
|
4
|
+
const { getMarkdownParser } = require('../service/markdown');
|
|
5
|
+
|
|
6
|
+
const markdownParser = getMarkdownParser();
|
|
7
|
+
|
|
8
|
+
async function formatCmt(
|
|
9
|
+
{ ua, ip, ...comment },
|
|
10
|
+
users = [],
|
|
11
|
+
{ avatarProxy },
|
|
12
|
+
loginUser
|
|
13
|
+
) {
|
|
14
|
+
ua = parser(ua);
|
|
15
|
+
if (!think.config('disableUserAgent')) {
|
|
16
|
+
comment.browser = `${ua.browser.name || ''}${(ua.browser.version || '')
|
|
17
|
+
.split('.')
|
|
18
|
+
.slice(0, 2)
|
|
19
|
+
.join('.')}`;
|
|
20
|
+
comment.os = [ua.os.name, ua.os.version].filter((v) => v).join(' ');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const user = users.find(({ objectId }) => comment.user_id === objectId);
|
|
24
|
+
|
|
25
|
+
if (!think.isEmpty(user)) {
|
|
26
|
+
comment.nick = user.display_name;
|
|
27
|
+
comment.mail = user.email;
|
|
28
|
+
comment.link = user.url;
|
|
29
|
+
comment.type = user.type;
|
|
30
|
+
comment.label = user.label;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const avatarUrl =
|
|
34
|
+
user && user.avatar
|
|
35
|
+
? user.avatar
|
|
36
|
+
: await think.service('avatar').stringify(comment);
|
|
37
|
+
|
|
38
|
+
comment.avatar =
|
|
39
|
+
avatarProxy && !avatarUrl.includes(avatarProxy)
|
|
40
|
+
? avatarProxy + '?url=' + encodeURIComponent(avatarUrl)
|
|
41
|
+
: avatarUrl;
|
|
42
|
+
|
|
43
|
+
const isAdmin = loginUser && loginUser.type === 'administrator';
|
|
44
|
+
|
|
45
|
+
if (loginUser) {
|
|
46
|
+
comment.orig = comment.comment;
|
|
47
|
+
}
|
|
48
|
+
if (!isAdmin) {
|
|
49
|
+
delete comment.mail;
|
|
50
|
+
} else {
|
|
51
|
+
comment.ip = ip;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// administrator can always show region
|
|
55
|
+
if (isAdmin || !think.config('disableRegion')) {
|
|
56
|
+
comment.addr = await think.ip2region(ip, { depth: isAdmin ? 3 : 1 });
|
|
57
|
+
}
|
|
58
|
+
comment.comment = markdownParser(comment.comment);
|
|
59
|
+
comment.like = Number(comment.like) || 0;
|
|
60
|
+
|
|
61
|
+
// compat sql storage return number flag to string
|
|
62
|
+
if (typeof comment.sticky === 'string') {
|
|
63
|
+
comment.sticky = Boolean(Number(comment.sticky));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return comment;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = class extends BaseRest {
|
|
70
|
+
constructor(ctx) {
|
|
71
|
+
super(ctx);
|
|
72
|
+
this.modelInstance = this.service(
|
|
73
|
+
`storage/${this.config('storage')}`,
|
|
74
|
+
'Comment'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getAction() {
|
|
79
|
+
const { type } = this.get();
|
|
80
|
+
const { userInfo } = this.ctx.state;
|
|
81
|
+
|
|
82
|
+
switch (type) {
|
|
83
|
+
case 'recent': {
|
|
84
|
+
const { count } = this.get();
|
|
85
|
+
const where = {};
|
|
86
|
+
|
|
87
|
+
if (think.isEmpty(userInfo) || this.config('storage') === 'deta') {
|
|
88
|
+
where.status = ['NOT IN', ['waiting', 'spam']];
|
|
89
|
+
} else {
|
|
90
|
+
where._complex = {
|
|
91
|
+
_logic: 'or',
|
|
92
|
+
status: ['NOT IN', ['waiting', 'spam']],
|
|
93
|
+
user_id: userInfo.objectId,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const comments = await this.modelInstance.select(where, {
|
|
98
|
+
desc: 'insertedAt',
|
|
99
|
+
limit: count,
|
|
100
|
+
field: [
|
|
101
|
+
'status',
|
|
102
|
+
'comment',
|
|
103
|
+
'insertedAt',
|
|
104
|
+
'link',
|
|
105
|
+
'mail',
|
|
106
|
+
'nick',
|
|
107
|
+
'url',
|
|
108
|
+
'pid',
|
|
109
|
+
'rid',
|
|
110
|
+
'ua',
|
|
111
|
+
'ip',
|
|
112
|
+
'user_id',
|
|
113
|
+
'sticky',
|
|
114
|
+
'like',
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const userModel = this.service(
|
|
119
|
+
`storage/${this.config('storage')}`,
|
|
120
|
+
'Users'
|
|
121
|
+
);
|
|
122
|
+
const user_ids = Array.from(
|
|
123
|
+
new Set(comments.map(({ user_id }) => user_id).filter((v) => v))
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
let users = [];
|
|
127
|
+
|
|
128
|
+
if (user_ids.length) {
|
|
129
|
+
users = await userModel.select(
|
|
130
|
+
{ objectId: ['IN', user_ids] },
|
|
131
|
+
{
|
|
132
|
+
field: [
|
|
133
|
+
'display_name',
|
|
134
|
+
'email',
|
|
135
|
+
'url',
|
|
136
|
+
'type',
|
|
137
|
+
'avatar',
|
|
138
|
+
'label',
|
|
139
|
+
],
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return this.json(
|
|
145
|
+
await Promise.all(
|
|
146
|
+
comments.map((cmt) =>
|
|
147
|
+
formatCmt(cmt, users, this.config(), userInfo)
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
case 'count': {
|
|
154
|
+
const { url } = this.get();
|
|
155
|
+
const where =
|
|
156
|
+
Array.isArray(url) && url.length ? { url: ['IN', url] } : {};
|
|
157
|
+
|
|
158
|
+
if (think.isEmpty(userInfo) || this.config('storage') === 'deta') {
|
|
159
|
+
where.status = ['NOT IN', ['waiting', 'spam']];
|
|
160
|
+
} else {
|
|
161
|
+
where._complex = {
|
|
162
|
+
_logic: 'or',
|
|
163
|
+
status: ['NOT IN', ['waiting', 'spam']],
|
|
164
|
+
user_id: userInfo.objectId,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (Array.isArray(url) && url.length > 1) {
|
|
169
|
+
const data = await this.modelInstance.select(where, {
|
|
170
|
+
field: ['url'],
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return this.json(
|
|
174
|
+
url.map((u) => data.filter(({ url }) => url === u).length)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const data = await this.modelInstance.count(where);
|
|
178
|
+
|
|
179
|
+
return this.json(data);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case 'list': {
|
|
183
|
+
const { page, pageSize, owner, status, keyword } = this.get();
|
|
184
|
+
const where = {};
|
|
185
|
+
|
|
186
|
+
if (owner === 'mine') {
|
|
187
|
+
const { userInfo } = this.ctx.state;
|
|
188
|
+
|
|
189
|
+
where.mail = userInfo.email;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (status) {
|
|
193
|
+
where.status = status;
|
|
194
|
+
|
|
195
|
+
// compat with valine old data without status property
|
|
196
|
+
if (status === 'approved') {
|
|
197
|
+
where.status = ['NOT IN', ['waiting', 'spam']];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (keyword) {
|
|
202
|
+
where.comment = ['LIKE', `%${keyword}%`];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const count = await this.modelInstance.count(where);
|
|
206
|
+
const spamCount = await this.modelInstance.count({ status: 'spam' });
|
|
207
|
+
const waitingCount = await this.modelInstance.count({
|
|
208
|
+
status: 'waiting',
|
|
209
|
+
});
|
|
210
|
+
const comments = await this.modelInstance.select(where, {
|
|
211
|
+
desc: 'insertedAt',
|
|
212
|
+
limit: pageSize,
|
|
213
|
+
offset: Math.max((page - 1) * pageSize, 0),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const userModel = this.service(
|
|
217
|
+
`storage/${this.config('storage')}`,
|
|
218
|
+
'Users'
|
|
219
|
+
);
|
|
220
|
+
const user_ids = Array.from(
|
|
221
|
+
new Set(comments.map(({ user_id }) => user_id).filter((v) => v))
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
let users = [];
|
|
225
|
+
|
|
226
|
+
if (user_ids.length) {
|
|
227
|
+
users = await userModel.select(
|
|
228
|
+
{ objectId: ['IN', user_ids] },
|
|
229
|
+
{
|
|
230
|
+
field: [
|
|
231
|
+
'display_name',
|
|
232
|
+
'email',
|
|
233
|
+
'url',
|
|
234
|
+
'type',
|
|
235
|
+
'avatar',
|
|
236
|
+
'label',
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return this.success({
|
|
243
|
+
page,
|
|
244
|
+
totalPages: Math.ceil(count / pageSize),
|
|
245
|
+
pageSize,
|
|
246
|
+
spamCount,
|
|
247
|
+
waitingCount,
|
|
248
|
+
data: await Promise.all(
|
|
249
|
+
comments.map((cmt) =>
|
|
250
|
+
formatCmt(cmt, users, this.config(), userInfo)
|
|
251
|
+
)
|
|
252
|
+
),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
default: {
|
|
257
|
+
const { path: url, page, pageSize, sortBy } = this.get();
|
|
258
|
+
const where = { url };
|
|
259
|
+
|
|
260
|
+
if (think.isEmpty(userInfo) || this.config('storage') === 'deta') {
|
|
261
|
+
where.status = ['NOT IN', ['waiting', 'spam']];
|
|
262
|
+
} else if (userInfo.type !== 'administrator') {
|
|
263
|
+
where._complex = {
|
|
264
|
+
_logic: 'or',
|
|
265
|
+
status: ['NOT IN', ['waiting', 'spam']],
|
|
266
|
+
user_id: userInfo.objectId,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const totalCount = await this.modelInstance.count(where);
|
|
271
|
+
const pageOffset = Math.max((page - 1) * pageSize, 0);
|
|
272
|
+
let comments = [];
|
|
273
|
+
let rootComments = [];
|
|
274
|
+
let rootCount = 0;
|
|
275
|
+
const selectOptions = {
|
|
276
|
+
field: [
|
|
277
|
+
'status',
|
|
278
|
+
'comment',
|
|
279
|
+
'insertedAt',
|
|
280
|
+
'link',
|
|
281
|
+
'mail',
|
|
282
|
+
'nick',
|
|
283
|
+
'pid',
|
|
284
|
+
'rid',
|
|
285
|
+
'ua',
|
|
286
|
+
'ip',
|
|
287
|
+
'user_id',
|
|
288
|
+
'sticky',
|
|
289
|
+
'like',
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
if (sortBy) {
|
|
294
|
+
const [field, order] = sortBy.split('_');
|
|
295
|
+
|
|
296
|
+
if (order === 'desc') {
|
|
297
|
+
selectOptions.desc = field;
|
|
298
|
+
} else if (order === 'asc') {
|
|
299
|
+
// do nothing because of ascending order is default behavior
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* most of case we have just little comments
|
|
305
|
+
* while if we want get rootComments, rootCount, childComments with pagination
|
|
306
|
+
* we have to query three times from storage service
|
|
307
|
+
* That's so expensive for user, especially in the serverless.
|
|
308
|
+
* so we have a comments length check
|
|
309
|
+
* If you have less than 1000 comments, then we'll get all comments one time
|
|
310
|
+
* then we'll compute rootComment, rootCount, childComments in program to reduce http request query
|
|
311
|
+
*
|
|
312
|
+
* Why we have limit and the limit is 1000?
|
|
313
|
+
* Many serverless storages have fetch data limit, for example LeanCloud is 100, and CloudBase is 1000
|
|
314
|
+
* If we have much comments, We should use more request to fetch all comments
|
|
315
|
+
* If we have 3000 comments, We have to use 30 http request to fetch comments, things go athwart.
|
|
316
|
+
* And Serverless Service like vercel have execute time limit
|
|
317
|
+
* if we have more http requests in a serverless function, it may timeout easily.
|
|
318
|
+
* so we use limit to avoid it.
|
|
319
|
+
*/
|
|
320
|
+
if (totalCount < 1000) {
|
|
321
|
+
comments = await this.modelInstance.select(where, selectOptions);
|
|
322
|
+
rootCount = comments.filter(({ rid }) => !rid).length;
|
|
323
|
+
rootComments = [
|
|
324
|
+
...comments.filter(({ rid, sticky }) => !rid && sticky),
|
|
325
|
+
...comments.filter(({ rid, sticky }) => !rid && !sticky),
|
|
326
|
+
].slice(pageOffset, pageOffset + pageSize);
|
|
327
|
+
const rootIds = {};
|
|
328
|
+
|
|
329
|
+
rootComments.forEach(({ objectId }) => {
|
|
330
|
+
rootIds[objectId] = true;
|
|
331
|
+
});
|
|
332
|
+
comments = comments.filter(
|
|
333
|
+
(cmt) => rootIds[cmt.objectId] || rootIds[cmt.rid]
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
rootComments = await this.modelInstance.select(
|
|
337
|
+
{ ...where, rid: undefined },
|
|
338
|
+
{
|
|
339
|
+
...selectOptions,
|
|
340
|
+
offset: pageOffset,
|
|
341
|
+
limit: pageSize,
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
const children = await this.modelInstance.select(
|
|
345
|
+
{
|
|
346
|
+
...where,
|
|
347
|
+
rid: ['IN', rootComments.map(({ objectId }) => objectId)],
|
|
348
|
+
},
|
|
349
|
+
selectOptions
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
comments = [...rootComments, ...children];
|
|
353
|
+
rootCount = await this.modelInstance.count({
|
|
354
|
+
...where,
|
|
355
|
+
rid: undefined,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const userModel = this.service(
|
|
360
|
+
`storage/${this.config('storage')}`,
|
|
361
|
+
'Users'
|
|
362
|
+
);
|
|
363
|
+
const user_ids = Array.from(
|
|
364
|
+
new Set(comments.map(({ user_id }) => user_id).filter((v) => v))
|
|
365
|
+
);
|
|
366
|
+
let users = [];
|
|
367
|
+
|
|
368
|
+
if (user_ids.length) {
|
|
369
|
+
users = await userModel.select(
|
|
370
|
+
{ objectId: ['IN', user_ids] },
|
|
371
|
+
{
|
|
372
|
+
field: [
|
|
373
|
+
'display_name',
|
|
374
|
+
'email',
|
|
375
|
+
'url',
|
|
376
|
+
'type',
|
|
377
|
+
'avatar',
|
|
378
|
+
'label',
|
|
379
|
+
],
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (think.isArray(this.config('levels'))) {
|
|
385
|
+
const countWhere = {
|
|
386
|
+
status: ['NOT IN', ['waiting', 'spam']],
|
|
387
|
+
_complex: {},
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (user_ids.length) {
|
|
391
|
+
countWhere._complex.user_id = ['IN', user_ids];
|
|
392
|
+
}
|
|
393
|
+
const mails = Array.from(
|
|
394
|
+
new Set(comments.map(({ mail }) => mail).filter((v) => v))
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
if (mails.length) {
|
|
398
|
+
countWhere._complex.mail = ['IN', mails];
|
|
399
|
+
}
|
|
400
|
+
if (!think.isEmpty(countWhere._complex)) {
|
|
401
|
+
countWhere._complex._logic = 'or';
|
|
402
|
+
} else {
|
|
403
|
+
delete countWhere._complex;
|
|
404
|
+
}
|
|
405
|
+
const counts = await this.modelInstance.count(countWhere, {
|
|
406
|
+
group: ['user_id', 'mail'],
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
comments.forEach((cmt) => {
|
|
410
|
+
const countItem = (counts || []).find(({ mail, user_id }) => {
|
|
411
|
+
if (cmt.user_id) {
|
|
412
|
+
return user_id === cmt.user_id;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return mail === cmt.mail;
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
let level = 0;
|
|
419
|
+
|
|
420
|
+
if (countItem) {
|
|
421
|
+
const _level = think.findLastIndex(
|
|
422
|
+
this.config('levels'),
|
|
423
|
+
(l) => l <= countItem.count
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
if (_level !== -1) {
|
|
427
|
+
level = _level;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
cmt.level = level;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return this.json({
|
|
435
|
+
page,
|
|
436
|
+
totalPages: Math.ceil(rootCount / pageSize),
|
|
437
|
+
pageSize,
|
|
438
|
+
count: totalCount,
|
|
439
|
+
data: await Promise.all(
|
|
440
|
+
rootComments.map(async (comment) => {
|
|
441
|
+
const cmt = await formatCmt(
|
|
442
|
+
comment,
|
|
443
|
+
users,
|
|
444
|
+
this.config(),
|
|
445
|
+
userInfo
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
cmt.children = await Promise.all(
|
|
449
|
+
comments
|
|
450
|
+
.filter(({ rid }) => rid === cmt.objectId)
|
|
451
|
+
.map((cmt) => formatCmt(cmt, users, this.config(), userInfo))
|
|
452
|
+
.reverse()
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
return cmt;
|
|
456
|
+
})
|
|
457
|
+
),
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async postAction() {
|
|
464
|
+
think.logger.debug('Post Comment Start!');
|
|
465
|
+
|
|
466
|
+
const { comment, link, mail, nick, pid, rid, ua, url, at } = this.post();
|
|
467
|
+
const data = {
|
|
468
|
+
link,
|
|
469
|
+
mail,
|
|
470
|
+
nick,
|
|
471
|
+
pid,
|
|
472
|
+
rid,
|
|
473
|
+
ua,
|
|
474
|
+
url,
|
|
475
|
+
comment,
|
|
476
|
+
ip: this.ctx.ip,
|
|
477
|
+
insertedAt: new Date(),
|
|
478
|
+
user_id: this.ctx.state.userInfo.objectId,
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
if (pid) {
|
|
482
|
+
data.comment = `[@${at}](#${pid}): ` + data.comment;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
think.logger.debug('Post Comment initial Data:', data);
|
|
486
|
+
|
|
487
|
+
const { userInfo } = this.ctx.state;
|
|
488
|
+
|
|
489
|
+
if (!userInfo || userInfo.type !== 'administrator') {
|
|
490
|
+
/** IP disallowList */
|
|
491
|
+
const { disallowIPList } = this.config();
|
|
492
|
+
|
|
493
|
+
if (
|
|
494
|
+
think.isArray(disallowIPList) &&
|
|
495
|
+
disallowIPList.length &&
|
|
496
|
+
disallowIPList.includes(data.ip)
|
|
497
|
+
) {
|
|
498
|
+
think.logger.debug(`Comment IP ${data.ip} is in disallowIPList`);
|
|
499
|
+
|
|
500
|
+
return this.ctx.throw(403);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
think.logger.debug(`Comment IP ${data.ip} check OK!`);
|
|
504
|
+
|
|
505
|
+
/** Duplicate content detect */
|
|
506
|
+
const duplicate = await this.modelInstance.select({
|
|
507
|
+
url,
|
|
508
|
+
mail: data.mail,
|
|
509
|
+
nick: data.nick,
|
|
510
|
+
link: data.link,
|
|
511
|
+
comment: data.comment,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
if (!think.isEmpty(duplicate)) {
|
|
515
|
+
think.logger.debug(
|
|
516
|
+
'The comment author had post same comment content before'
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
return this.fail(this.locale('Duplicate Content'));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
think.logger.debug('Comment duplicate check OK!');
|
|
523
|
+
|
|
524
|
+
/** IP frequency */
|
|
525
|
+
const { IPQPS = 60 } = process.env;
|
|
526
|
+
|
|
527
|
+
const recent = await this.modelInstance.select({
|
|
528
|
+
ip: this.ctx.ip,
|
|
529
|
+
insertedAt: ['>', new Date(Date.now() - IPQPS * 1000)],
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
if (!think.isEmpty(recent)) {
|
|
533
|
+
think.logger.debug(`The author has posted in ${IPQPS} seconds.`);
|
|
534
|
+
|
|
535
|
+
return this.fail(this.locale('Comment too fast!'));
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
think.logger.debug(`Comment post frequency check OK!`);
|
|
539
|
+
|
|
540
|
+
/** Akismet */
|
|
541
|
+
const { COMMENT_AUDIT } = process.env;
|
|
542
|
+
|
|
543
|
+
data.status = COMMENT_AUDIT ? 'waiting' : 'approved';
|
|
544
|
+
|
|
545
|
+
think.logger.debug(`Comment initial status is ${data.status}`);
|
|
546
|
+
|
|
547
|
+
if (data.status === 'approved') {
|
|
548
|
+
const spam = await akismet(data, this.ctx.serverURL).catch((e) =>
|
|
549
|
+
console.log(e)
|
|
550
|
+
); // ignore akismet error
|
|
551
|
+
|
|
552
|
+
if (spam === true) {
|
|
553
|
+
data.status = 'spam';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
think.logger.debug(`Comment akismet check result: ${data.status}`);
|
|
558
|
+
|
|
559
|
+
if (data.status !== 'spam') {
|
|
560
|
+
/** KeyWord Filter */
|
|
561
|
+
const { forbiddenWords } = this.config();
|
|
562
|
+
|
|
563
|
+
if (!think.isEmpty(forbiddenWords)) {
|
|
564
|
+
const regexp = new RegExp('(' + forbiddenWords.join('|') + ')', 'ig');
|
|
565
|
+
|
|
566
|
+
if (regexp.test(comment)) {
|
|
567
|
+
data.status = 'spam';
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
think.logger.debug(`Comment keyword check result: ${data.status}`);
|
|
573
|
+
} else {
|
|
574
|
+
data.status = 'approved';
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const preSaveResp = await this.hook('preSave', data);
|
|
578
|
+
|
|
579
|
+
if (preSaveResp) {
|
|
580
|
+
return this.fail(preSaveResp.errmsg);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
think.logger.debug(`Comment post hooks preSave done!`);
|
|
584
|
+
|
|
585
|
+
const resp = await this.modelInstance.add(data);
|
|
586
|
+
|
|
587
|
+
think.logger.debug(`Comment have been added to storage.`);
|
|
588
|
+
|
|
589
|
+
let parentComment;
|
|
590
|
+
let parentUser;
|
|
591
|
+
|
|
592
|
+
if (pid) {
|
|
593
|
+
parentComment = await this.modelInstance.select({ objectId: pid });
|
|
594
|
+
parentComment = parentComment[0];
|
|
595
|
+
if (parentComment.user_id) {
|
|
596
|
+
parentUser = await this.service(
|
|
597
|
+
`storage/${this.config('storage')}`,
|
|
598
|
+
'Users'
|
|
599
|
+
).select({
|
|
600
|
+
objectId: parentComment.user_id,
|
|
601
|
+
});
|
|
602
|
+
parentUser = parentUser[0];
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
await this.ctx.webhook('new_comment', {
|
|
607
|
+
comment: { ...resp, rawComment: comment },
|
|
608
|
+
reply: parentComment,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const cmtReturn = await formatCmt(
|
|
612
|
+
resp,
|
|
613
|
+
[userInfo],
|
|
614
|
+
this.config(),
|
|
615
|
+
userInfo
|
|
616
|
+
);
|
|
617
|
+
const parentReturn = parentComment
|
|
618
|
+
? await formatCmt(
|
|
619
|
+
parentComment,
|
|
620
|
+
parentUser ? [parentUser] : [],
|
|
621
|
+
this.config(),
|
|
622
|
+
userInfo
|
|
623
|
+
)
|
|
624
|
+
: undefined;
|
|
625
|
+
|
|
626
|
+
if (comment.status !== 'spam') {
|
|
627
|
+
const notify = this.service('notify', this);
|
|
628
|
+
|
|
629
|
+
await notify.run(
|
|
630
|
+
{ ...cmtReturn, mail: resp.mail, rawComment: comment },
|
|
631
|
+
parentReturn ? { ...parentReturn, mail: parentComment.mail } : undefined
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
think.logger.debug(`Comment notify done!`);
|
|
636
|
+
|
|
637
|
+
await this.hook('postSave', resp, parentComment);
|
|
638
|
+
|
|
639
|
+
think.logger.debug(`Comment post hooks postSave done!`);
|
|
640
|
+
|
|
641
|
+
return this.success(
|
|
642
|
+
await formatCmt(resp, [userInfo], this.config(), userInfo)
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async putAction() {
|
|
647
|
+
const { userInfo } = this.ctx.state;
|
|
648
|
+
const isAdmin = userInfo.type === 'administrator';
|
|
649
|
+
let data = isAdmin ? this.post() : this.post('comment,like');
|
|
650
|
+
let oldData = await this.modelInstance.select({ objectId: this.id });
|
|
651
|
+
|
|
652
|
+
if (think.isEmpty(oldData) || think.isEmpty(data)) {
|
|
653
|
+
return this.success();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
oldData = oldData[0];
|
|
657
|
+
if (think.isBoolean(data.like)) {
|
|
658
|
+
const likeIncMax = this.config('LIKE_INC_MAX') || 1;
|
|
659
|
+
|
|
660
|
+
data.like =
|
|
661
|
+
(Number(oldData.like) || 0) +
|
|
662
|
+
(data.like ? Math.ceil(Math.random() * likeIncMax) : -1);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const preUpdateResp = await this.hook('preUpdate', {
|
|
666
|
+
...data,
|
|
667
|
+
objectId: this.id,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
if (preUpdateResp) {
|
|
671
|
+
return this.fail(preUpdateResp);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const newData = await this.modelInstance.update(data, {
|
|
675
|
+
objectId: this.id,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
let cmtUser;
|
|
679
|
+
|
|
680
|
+
if (!think.isEmpty(newData) && newData[0].user_id) {
|
|
681
|
+
cmtUser = await this.service(
|
|
682
|
+
`storage/${this.config('storage')}`,
|
|
683
|
+
'Users'
|
|
684
|
+
).select({
|
|
685
|
+
objectId: newData[0].user_id,
|
|
686
|
+
});
|
|
687
|
+
cmtUser = cmtUser[0];
|
|
688
|
+
}
|
|
689
|
+
const cmtReturn = await formatCmt(
|
|
690
|
+
newData[0],
|
|
691
|
+
cmtUser ? [cmtUser] : [],
|
|
692
|
+
this.config(),
|
|
693
|
+
userInfo
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
if (
|
|
697
|
+
oldData.status === 'waiting' &&
|
|
698
|
+
data.status === 'approved' &&
|
|
699
|
+
oldData.pid
|
|
700
|
+
) {
|
|
701
|
+
let pComment = await this.modelInstance.select({
|
|
702
|
+
objectId: oldData.pid,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
pComment = pComment[0];
|
|
706
|
+
|
|
707
|
+
let pUser;
|
|
708
|
+
|
|
709
|
+
if (pComment.user_id) {
|
|
710
|
+
pUser = await this.service(
|
|
711
|
+
`storage/${this.config('storage')}`,
|
|
712
|
+
'Users'
|
|
713
|
+
).select({
|
|
714
|
+
objectId: pComment.user_id,
|
|
715
|
+
});
|
|
716
|
+
pUser = pUser[0];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const notify = this.service('notify', this);
|
|
720
|
+
const pcmtReturn = await formatCmt(
|
|
721
|
+
pComment,
|
|
722
|
+
pUser ? [pUser] : [],
|
|
723
|
+
this.config(),
|
|
724
|
+
userInfo
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
await notify.run(
|
|
728
|
+
{ ...cmtReturn, mail: newData[0].mail },
|
|
729
|
+
{ ...pcmtReturn, mail: pComment.mail },
|
|
730
|
+
true
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
await this.hook('postUpdate', data);
|
|
735
|
+
|
|
736
|
+
return this.success(cmtReturn);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async deleteAction() {
|
|
740
|
+
const preDeleteResp = await this.hook('preDelete', this.id);
|
|
741
|
+
|
|
742
|
+
if (preDeleteResp) {
|
|
743
|
+
return this.fail(preDeleteResp);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
await this.modelInstance.delete({
|
|
747
|
+
_complex: {
|
|
748
|
+
_logic: 'or',
|
|
749
|
+
objectId: this.id,
|
|
750
|
+
pid: this.id,
|
|
751
|
+
rid: this.id,
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
await this.hook('postDelete', this.id);
|
|
755
|
+
|
|
756
|
+
return this.success();
|
|
757
|
+
}
|
|
758
|
+
};
|