@waline/vercel 1.30.4 → 1.31.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waline/vercel",
3
- "version": "1.30.4",
3
+ "version": "1.31.0",
4
4
  "description": "vercel server for waline comment system",
5
5
  "keywords": [
6
6
  "waline",
@@ -19,21 +19,22 @@
19
19
  "@koa/cors": "^4.0.0",
20
20
  "akismet": "^2.0.7",
21
21
  "deta": "^1.1.0",
22
- "dompurify": "^3.0.2",
22
+ "dompurify": "^3.0.3",
23
23
  "dy-node-ip2region": "^1.0.1",
24
24
  "fast-csv": "^4.3.6",
25
25
  "form-data": "^4.0.0",
26
- "jsdom": "^21.1.1",
26
+ "jsdom": "^22.1.0",
27
27
  "jsonwebtoken": "^9.0.0",
28
- "katex": "^0.16.6",
28
+ "katex": "^0.16.7",
29
+ "koa-compose": "^4.1.0",
29
30
  "leancloud-storage": "^4.15.0",
30
31
  "markdown-it": "^13.0.1",
31
32
  "markdown-it-emoji": "^2.0.2",
32
33
  "markdown-it-sub": "^1.0.0",
33
34
  "markdown-it-sup": "^1.0.0",
34
35
  "mathjax-full": "^3.2.2",
35
- "node-fetch": "^2.6.9",
36
- "nodemailer": "^6.9.1",
36
+ "node-fetch": "^2.6.11",
37
+ "nodemailer": "^6.9.2",
37
38
  "nunjucks": "^3.2.4",
38
39
  "phpass": "^0.1.1",
39
40
  "prismjs": "^1.29.0",
@@ -45,6 +45,7 @@ const {
45
45
  LARK_TEMPLATE,
46
46
 
47
47
  LEVELS,
48
+ COMMENT_AUDIT,
48
49
  } = process.env;
49
50
 
50
51
  let storage = 'leancloud';
@@ -121,6 +122,8 @@ module.exports = {
121
122
  !LEVELS || isFalse(LEVELS)
122
123
  ? false
123
124
  : LEVELS.split(/\s*,\s*/).map((v) => Number(v)),
125
+
126
+ audit: !isFalse(COMMENT_AUDIT),
124
127
  avatarProxy,
125
128
  oauthUrl,
126
129
  markdown,
@@ -71,5 +71,8 @@ module.exports = [
71
71
  { handle: routerREST },
72
72
 
73
73
  'logic',
74
+ {
75
+ handle: 'plugin',
76
+ },
74
77
  'controller',
75
78
  ];
@@ -80,399 +80,17 @@ module.exports = class extends BaseRest {
80
80
 
81
81
  async getAction() {
82
82
  const { type } = this.get();
83
- const { userInfo } = this.ctx.state;
84
-
85
- switch (type) {
86
- case 'recent': {
87
- const { count } = this.get();
88
- const where = {};
89
-
90
- if (think.isEmpty(userInfo) || this.config('storage') === 'deta') {
91
- where.status = ['NOT IN', ['waiting', 'spam']];
92
- } else {
93
- where._complex = {
94
- _logic: 'or',
95
- status: ['NOT IN', ['waiting', 'spam']],
96
- user_id: userInfo.objectId,
97
- };
98
- }
99
-
100
- const comments = await this.modelInstance.select(where, {
101
- desc: 'insertedAt',
102
- limit: count,
103
- field: [
104
- 'status',
105
- 'comment',
106
- 'insertedAt',
107
- 'link',
108
- 'mail',
109
- 'nick',
110
- 'url',
111
- 'pid',
112
- 'rid',
113
- 'ua',
114
- 'ip',
115
- 'user_id',
116
- 'sticky',
117
- 'like',
118
- ],
119
- });
120
-
121
- const userModel = this.getModel('Users');
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.jsonOrSuccess(
145
- await Promise.all(
146
- comments.map((cmt) =>
147
- formatCmt(
148
- cmt,
149
- users,
150
- { ...this.config(), deprecated: this.ctx.state.deprecated },
151
- userInfo
152
- )
153
- )
154
- )
155
- );
156
- }
157
-
158
- case 'count': {
159
- const { url } = this.get();
160
- const where =
161
- Array.isArray(url) && url.length ? { url: ['IN', url] } : {};
162
-
163
- if (think.isEmpty(userInfo) || this.config('storage') === 'deta') {
164
- where.status = ['NOT IN', ['waiting', 'spam']];
165
- } else {
166
- where._complex = {
167
- _logic: 'or',
168
- status: ['NOT IN', ['waiting', 'spam']],
169
- user_id: userInfo.objectId,
170
- };
171
- }
172
-
173
- if (
174
- Array.isArray(url) &&
175
- (url.length > 1 || !this.ctx.state.deprecated)
176
- ) {
177
- const data = await this.modelInstance.select(where, {
178
- field: ['url'],
179
- });
180
-
181
- return this.jsonOrSuccess(
182
- url.map((u) => data.filter(({ url }) => url === u).length)
183
- );
184
- }
185
- const data = await this.modelInstance.count(where);
186
-
187
- return this.jsonOrSuccess(data);
188
- }
189
-
190
- case 'list': {
191
- const { page, pageSize, owner, status, keyword } = this.get();
192
- const where = {};
193
-
194
- if (owner === 'mine') {
195
- const { userInfo } = this.ctx.state;
196
-
197
- where.mail = userInfo.email;
198
- }
199
-
200
- if (status) {
201
- where.status = status;
202
-
203
- // compat with valine old data without status property
204
- if (status === 'approved') {
205
- where.status = ['NOT IN', ['waiting', 'spam']];
206
- }
207
- }
208
-
209
- if (keyword) {
210
- where.comment = ['LIKE', `%${keyword}%`];
211
- }
212
-
213
- const count = await this.modelInstance.count(where);
214
- const spamCount = await this.modelInstance.count({ status: 'spam' });
215
- const waitingCount = await this.modelInstance.count({
216
- status: 'waiting',
217
- });
218
- const comments = await this.modelInstance.select(where, {
219
- desc: 'insertedAt',
220
- limit: pageSize,
221
- offset: Math.max((page - 1) * pageSize, 0),
222
- });
223
-
224
- const userModel = this.getModel('Users');
225
- const user_ids = Array.from(
226
- new Set(comments.map(({ user_id }) => user_id).filter((v) => v))
227
- );
228
-
229
- let users = [];
230
-
231
- if (user_ids.length) {
232
- users = await userModel.select(
233
- { objectId: ['IN', user_ids] },
234
- {
235
- field: [
236
- 'display_name',
237
- 'email',
238
- 'url',
239
- 'type',
240
- 'avatar',
241
- 'label',
242
- ],
243
- }
244
- );
245
- }
246
-
247
- return this.success({
248
- page,
249
- totalPages: Math.ceil(count / pageSize),
250
- pageSize,
251
- spamCount,
252
- waitingCount,
253
- data: await Promise.all(
254
- comments.map((cmt) =>
255
- formatCmt(
256
- cmt,
257
- users,
258
- { ...this.config(), deprecated: this.ctx.state.deprecated },
259
- userInfo
260
- )
261
- )
262
- ),
263
- });
264
- }
265
83
 
266
- default: {
267
- const { path: url, page, pageSize, sortBy } = this.get();
268
- const where = { url };
269
-
270
- if (think.isEmpty(userInfo) || this.config('storage') === 'deta') {
271
- where.status = ['NOT IN', ['waiting', 'spam']];
272
- } else if (userInfo.type !== 'administrator') {
273
- where._complex = {
274
- _logic: 'or',
275
- status: ['NOT IN', ['waiting', 'spam']],
276
- user_id: userInfo.objectId,
277
- };
278
- }
279
-
280
- const totalCount = await this.modelInstance.count(where);
281
- const pageOffset = Math.max((page - 1) * pageSize, 0);
282
- let comments = [];
283
- let rootComments = [];
284
- let rootCount = 0;
285
- const selectOptions = {
286
- field: [
287
- 'status',
288
- 'comment',
289
- 'insertedAt',
290
- 'link',
291
- 'mail',
292
- 'nick',
293
- 'pid',
294
- 'rid',
295
- 'ua',
296
- 'ip',
297
- 'user_id',
298
- 'sticky',
299
- 'like',
300
- ],
301
- };
302
-
303
- if (sortBy) {
304
- const [field, order] = sortBy.split('_');
305
-
306
- if (order === 'desc') {
307
- selectOptions.desc = field;
308
- } else if (order === 'asc') {
309
- // do nothing because of ascending order is default behavior
310
- }
311
- }
312
-
313
- /**
314
- * most of case we have just little comments
315
- * while if we want get rootComments, rootCount, childComments with pagination
316
- * we have to query three times from storage service
317
- * That's so expensive for user, especially in the serverless.
318
- * so we have a comments length check
319
- * If you have less than 1000 comments, then we'll get all comments one time
320
- * then we'll compute rootComment, rootCount, childComments in program to reduce http request query
321
- *
322
- * Why we have limit and the limit is 1000?
323
- * Many serverless storages have fetch data limit, for example LeanCloud is 100, and CloudBase is 1000
324
- * If we have much comments, We should use more request to fetch all comments
325
- * If we have 3000 comments, We have to use 30 http request to fetch comments, things go athwart.
326
- * And Serverless Service like vercel have execute time limit
327
- * if we have more http requests in a serverless function, it may timeout easily.
328
- * so we use limit to avoid it.
329
- */
330
- if (totalCount < 1000) {
331
- comments = await this.modelInstance.select(where, selectOptions);
332
- rootCount = comments.filter(({ rid }) => !rid).length;
333
- rootComments = [
334
- ...comments.filter(({ rid, sticky }) => !rid && sticky),
335
- ...comments.filter(({ rid, sticky }) => !rid && !sticky),
336
- ].slice(pageOffset, pageOffset + pageSize);
337
- const rootIds = {};
338
-
339
- rootComments.forEach(({ objectId }) => {
340
- rootIds[objectId] = true;
341
- });
342
- comments = comments.filter(
343
- (cmt) => rootIds[cmt.objectId] || rootIds[cmt.rid]
344
- );
345
- } else {
346
- comments = await this.modelInstance.select(
347
- { ...where, rid: undefined },
348
- { ...selectOptions }
349
- );
350
- rootCount = comments.length;
351
- rootComments = [
352
- ...comments.filter(({ rid, sticky }) => !rid && sticky),
353
- ...comments.filter(({ rid, sticky }) => !rid && !sticky),
354
- ].slice(pageOffset, pageOffset + pageSize);
355
-
356
- const children = await this.modelInstance.select(
357
- {
358
- ...where,
359
- rid: ['IN', rootComments.map(({ objectId }) => objectId)],
360
- },
361
- selectOptions
362
- );
363
-
364
- comments = [...rootComments, ...children];
365
- }
366
-
367
- const userModel = this.getModel('Users');
368
- const user_ids = Array.from(
369
- new Set(comments.map(({ user_id }) => user_id).filter((v) => v))
370
- );
371
- let users = [];
372
-
373
- if (user_ids.length) {
374
- users = await userModel.select(
375
- { objectId: ['IN', user_ids] },
376
- {
377
- field: [
378
- 'display_name',
379
- 'email',
380
- 'url',
381
- 'type',
382
- 'avatar',
383
- 'label',
384
- ],
385
- }
386
- );
387
- }
388
-
389
- if (think.isArray(this.config('levels'))) {
390
- const countWhere = {
391
- status: ['NOT IN', ['waiting', 'spam']],
392
- _complex: {},
393
- };
394
-
395
- if (user_ids.length) {
396
- countWhere._complex.user_id = ['IN', user_ids];
397
- }
398
- const mails = Array.from(
399
- new Set(comments.map(({ mail }) => mail).filter((v) => v))
400
- );
84
+ const fnMap = {
85
+ recent: this.getRecentCommentList,
86
+ count: this.getCommentCount,
87
+ list: this.getAdminCommentList,
88
+ };
401
89
 
402
- if (mails.length) {
403
- countWhere._complex.mail = ['IN', mails];
404
- }
405
- if (!think.isEmpty(countWhere._complex)) {
406
- countWhere._complex._logic = 'or';
407
- } else {
408
- delete countWhere._complex;
409
- }
410
- const counts = await this.modelInstance.count(countWhere, {
411
- group: ['user_id', 'mail'],
412
- });
413
-
414
- comments.forEach((cmt) => {
415
- const countItem = (counts || []).find(({ mail, user_id }) => {
416
- if (cmt.user_id) {
417
- return user_id === cmt.user_id;
418
- }
419
-
420
- return mail === cmt.mail;
421
- });
422
-
423
- let level = 0;
424
-
425
- if (countItem) {
426
- const _level = think.findLastIndex(
427
- this.config('levels'),
428
- (l) => l <= countItem.count
429
- );
430
-
431
- if (_level !== -1) {
432
- level = _level;
433
- }
434
- }
435
- cmt.level = level;
436
- });
437
- }
90
+ const fn = fnMap[type] || this.getCommentList;
91
+ const data = await fn.call(this);
438
92
 
439
- return this[this.ctx.state.deprecated ? 'json' : 'success']({
440
- page,
441
- totalPages: Math.ceil(rootCount / pageSize),
442
- pageSize,
443
- count: totalCount,
444
- data: await Promise.all(
445
- rootComments.map(async (comment) => {
446
- const cmt = await formatCmt(
447
- comment,
448
- users,
449
- { ...this.config(), deprecated: this.ctx.state.deprecated },
450
- userInfo
451
- );
452
-
453
- cmt.children = await Promise.all(
454
- comments
455
- .filter(({ rid }) => rid === cmt.objectId)
456
- .map((cmt) =>
457
- formatCmt(
458
- cmt,
459
- users,
460
- {
461
- ...this.config(),
462
- deprecated: this.ctx.state.deprecated,
463
- },
464
- userInfo
465
- )
466
- )
467
- .reverse()
468
- );
469
-
470
- return cmt;
471
- })
472
- ),
473
- });
474
- }
475
- }
93
+ return this.jsonOrSuccess(data);
476
94
  }
477
95
 
478
96
  async postAction() {
@@ -553,9 +171,7 @@ module.exports = class extends BaseRest {
553
171
  think.logger.debug(`Comment post frequency check OK!`);
554
172
 
555
173
  /** Akismet */
556
- const { COMMENT_AUDIT } = process.env;
557
-
558
- data.status = COMMENT_AUDIT ? 'waiting' : 'approved';
174
+ data.status = this.config('audit') ? 'waiting' : 'approved';
559
175
 
560
176
  think.logger.debug(`Comment initial status is ${data.status}`);
561
177
 
@@ -766,4 +382,353 @@ module.exports = class extends BaseRest {
766
382
 
767
383
  return this.success();
768
384
  }
385
+
386
+ async getCommentList() {
387
+ const { userInfo } = this.ctx.state;
388
+ const { path: url, page, pageSize, sortBy } = this.get();
389
+ const where = { url };
390
+
391
+ if (think.isEmpty(userInfo) || this.config('storage') === 'deta') {
392
+ where.status = ['NOT IN', ['waiting', 'spam']];
393
+ } else if (userInfo.type !== 'administrator') {
394
+ where._complex = {
395
+ _logic: 'or',
396
+ status: ['NOT IN', ['waiting', 'spam']],
397
+ user_id: userInfo.objectId,
398
+ };
399
+ }
400
+
401
+ const totalCount = await this.modelInstance.count(where);
402
+ const pageOffset = Math.max((page - 1) * pageSize, 0);
403
+ let comments = [];
404
+ let rootComments = [];
405
+ let rootCount = 0;
406
+ const selectOptions = {
407
+ field: [
408
+ 'status',
409
+ 'comment',
410
+ 'insertedAt',
411
+ 'link',
412
+ 'mail',
413
+ 'nick',
414
+ 'pid',
415
+ 'rid',
416
+ 'ua',
417
+ 'ip',
418
+ 'user_id',
419
+ 'sticky',
420
+ 'like',
421
+ ],
422
+ };
423
+
424
+ if (sortBy) {
425
+ const [field, order] = sortBy.split('_');
426
+
427
+ if (order === 'desc') {
428
+ selectOptions.desc = field;
429
+ } else if (order === 'asc') {
430
+ // do nothing because of ascending order is default behavior
431
+ }
432
+ }
433
+
434
+ /**
435
+ * most of case we have just little comments
436
+ * while if we want get rootComments, rootCount, childComments with pagination
437
+ * we have to query three times from storage service
438
+ * That's so expensive for user, especially in the serverless.
439
+ * so we have a comments length check
440
+ * If you have less than 1000 comments, then we'll get all comments one time
441
+ * then we'll compute rootComment, rootCount, childComments in program to reduce http request query
442
+ *
443
+ * Why we have limit and the limit is 1000?
444
+ * Many serverless storages have fetch data limit, for example LeanCloud is 100, and CloudBase is 1000
445
+ * If we have much comments, We should use more request to fetch all comments
446
+ * If we have 3000 comments, We have to use 30 http request to fetch comments, things go athwart.
447
+ * And Serverless Service like vercel have execute time limit
448
+ * if we have more http requests in a serverless function, it may timeout easily.
449
+ * so we use limit to avoid it.
450
+ */
451
+ if (totalCount < 1000) {
452
+ comments = await this.modelInstance.select(where, selectOptions);
453
+ rootCount = comments.filter(({ rid }) => !rid).length;
454
+ rootComments = [
455
+ ...comments.filter(({ rid, sticky }) => !rid && sticky),
456
+ ...comments.filter(({ rid, sticky }) => !rid && !sticky),
457
+ ].slice(pageOffset, pageOffset + pageSize);
458
+ const rootIds = {};
459
+
460
+ rootComments.forEach(({ objectId }) => {
461
+ rootIds[objectId] = true;
462
+ });
463
+ comments = comments.filter(
464
+ (cmt) => rootIds[cmt.objectId] || rootIds[cmt.rid]
465
+ );
466
+ } else {
467
+ comments = await this.modelInstance.select(
468
+ { ...where, rid: undefined },
469
+ { ...selectOptions }
470
+ );
471
+ rootCount = comments.length;
472
+ rootComments = [
473
+ ...comments.filter(({ rid, sticky }) => !rid && sticky),
474
+ ...comments.filter(({ rid, sticky }) => !rid && !sticky),
475
+ ].slice(pageOffset, pageOffset + pageSize);
476
+
477
+ const children = await this.modelInstance.select(
478
+ {
479
+ ...where,
480
+ rid: ['IN', rootComments.map(({ objectId }) => objectId)],
481
+ },
482
+ selectOptions
483
+ );
484
+
485
+ comments = [...rootComments, ...children];
486
+ }
487
+
488
+ const userModel = this.getModel('Users');
489
+ const user_ids = Array.from(
490
+ new Set(comments.map(({ user_id }) => user_id).filter((v) => v))
491
+ );
492
+ let users = [];
493
+
494
+ if (user_ids.length) {
495
+ users = await userModel.select(
496
+ { objectId: ['IN', user_ids] },
497
+ {
498
+ field: ['display_name', 'email', 'url', 'type', 'avatar', 'label'],
499
+ }
500
+ );
501
+ }
502
+
503
+ if (think.isArray(this.config('levels'))) {
504
+ const countWhere = {
505
+ status: ['NOT IN', ['waiting', 'spam']],
506
+ _complex: {},
507
+ };
508
+
509
+ if (user_ids.length) {
510
+ countWhere._complex.user_id = ['IN', user_ids];
511
+ }
512
+ const mails = Array.from(
513
+ new Set(comments.map(({ mail }) => mail).filter((v) => v))
514
+ );
515
+
516
+ if (mails.length) {
517
+ countWhere._complex.mail = ['IN', mails];
518
+ }
519
+ if (!think.isEmpty(countWhere._complex)) {
520
+ countWhere._complex._logic = 'or';
521
+ } else {
522
+ delete countWhere._complex;
523
+ }
524
+ const counts = await this.modelInstance.count(countWhere, {
525
+ group: ['user_id', 'mail'],
526
+ });
527
+
528
+ comments.forEach((cmt) => {
529
+ const countItem = (counts || []).find(({ mail, user_id }) =>
530
+ cmt.user_id ? user_id === cmt.user_id : mail === cmt.mail
531
+ );
532
+
533
+ cmt.level = think.getLevel(countItem?.count);
534
+ });
535
+ }
536
+
537
+ return {
538
+ page,
539
+ totalPages: Math.ceil(rootCount / pageSize),
540
+ pageSize,
541
+ count: totalCount,
542
+ data: await Promise.all(
543
+ rootComments.map(async (comment) => {
544
+ const cmt = await formatCmt(
545
+ comment,
546
+ users,
547
+ { ...this.config(), deprecated: this.ctx.state.deprecated },
548
+ userInfo
549
+ );
550
+
551
+ cmt.children = await Promise.all(
552
+ comments
553
+ .filter(({ rid }) => rid === cmt.objectId)
554
+ .map((cmt) =>
555
+ formatCmt(
556
+ cmt,
557
+ users,
558
+ {
559
+ ...this.config(),
560
+ deprecated: this.ctx.state.deprecated,
561
+ },
562
+ userInfo
563
+ )
564
+ )
565
+ .reverse()
566
+ );
567
+
568
+ return cmt;
569
+ })
570
+ ),
571
+ };
572
+ }
573
+
574
+ async getAdminCommentList() {
575
+ const { userInfo } = this.ctx.state;
576
+ const { page, pageSize, owner, status, keyword } = this.get();
577
+ const where = {};
578
+
579
+ if (owner === 'mine') {
580
+ const { userInfo } = this.ctx.state;
581
+
582
+ where.mail = userInfo.email;
583
+ }
584
+
585
+ if (status) {
586
+ where.status = status;
587
+
588
+ // compat with valine old data without status property
589
+ if (status === 'approved') {
590
+ where.status = ['NOT IN', ['waiting', 'spam']];
591
+ }
592
+ }
593
+
594
+ if (keyword) {
595
+ where.comment = ['LIKE', `%${keyword}%`];
596
+ }
597
+
598
+ const count = await this.modelInstance.count(where);
599
+ const spamCount = await this.modelInstance.count({ status: 'spam' });
600
+ const waitingCount = await this.modelInstance.count({
601
+ status: 'waiting',
602
+ });
603
+ const comments = await this.modelInstance.select(where, {
604
+ desc: 'insertedAt',
605
+ limit: pageSize,
606
+ offset: Math.max((page - 1) * pageSize, 0),
607
+ });
608
+
609
+ const userModel = this.getModel('Users');
610
+ const user_ids = Array.from(
611
+ new Set(comments.map(({ user_id }) => user_id).filter((v) => v))
612
+ );
613
+
614
+ let users = [];
615
+
616
+ if (user_ids.length) {
617
+ users = await userModel.select(
618
+ { objectId: ['IN', user_ids] },
619
+ {
620
+ field: ['display_name', 'email', 'url', 'type', 'avatar', 'label'],
621
+ }
622
+ );
623
+ }
624
+
625
+ return {
626
+ page,
627
+ totalPages: Math.ceil(count / pageSize),
628
+ pageSize,
629
+ spamCount,
630
+ waitingCount,
631
+ data: await Promise.all(
632
+ comments.map((cmt) =>
633
+ formatCmt(
634
+ cmt,
635
+ users,
636
+ { ...this.config(), deprecated: this.ctx.state.deprecated },
637
+ userInfo
638
+ )
639
+ )
640
+ ),
641
+ };
642
+ }
643
+
644
+ async getRecentCommentList() {
645
+ const { count } = this.get();
646
+ const { userInfo } = this.ctx.state;
647
+ const where = {};
648
+
649
+ if (think.isEmpty(userInfo) || this.config('storage') === 'deta') {
650
+ where.status = ['NOT IN', ['waiting', 'spam']];
651
+ } else {
652
+ where._complex = {
653
+ _logic: 'or',
654
+ status: ['NOT IN', ['waiting', 'spam']],
655
+ user_id: userInfo.objectId,
656
+ };
657
+ }
658
+
659
+ const comments = await this.modelInstance.select(where, {
660
+ desc: 'insertedAt',
661
+ limit: count,
662
+ field: [
663
+ 'status',
664
+ 'comment',
665
+ 'insertedAt',
666
+ 'link',
667
+ 'mail',
668
+ 'nick',
669
+ 'url',
670
+ 'pid',
671
+ 'rid',
672
+ 'ua',
673
+ 'ip',
674
+ 'user_id',
675
+ 'sticky',
676
+ 'like',
677
+ ],
678
+ });
679
+
680
+ const userModel = this.getModel('Users');
681
+ const user_ids = Array.from(
682
+ new Set(comments.map(({ user_id }) => user_id).filter((v) => v))
683
+ );
684
+
685
+ let users = [];
686
+
687
+ if (user_ids.length) {
688
+ users = await userModel.select(
689
+ { objectId: ['IN', user_ids] },
690
+ {
691
+ field: ['display_name', 'email', 'url', 'type', 'avatar', 'label'],
692
+ }
693
+ );
694
+ }
695
+
696
+ return Promise.all(
697
+ comments.map((cmt) =>
698
+ formatCmt(
699
+ cmt,
700
+ users,
701
+ { ...this.config(), deprecated: this.ctx.state.deprecated },
702
+ userInfo
703
+ )
704
+ )
705
+ );
706
+ }
707
+
708
+ async getCommentCount() {
709
+ const { url } = this.get();
710
+ const { userInfo } = this.ctx.state;
711
+ const where = Array.isArray(url) && url.length ? { url: ['IN', url] } : {};
712
+
713
+ if (think.isEmpty(userInfo) || this.config('storage') === 'deta') {
714
+ where.status = ['NOT IN', ['waiting', 'spam']];
715
+ } else {
716
+ where._complex = {
717
+ _logic: 'or',
718
+ status: ['NOT IN', ['waiting', 'spam']],
719
+ user_id: userInfo.objectId,
720
+ };
721
+ }
722
+
723
+ if (Array.isArray(url) && (url.length > 1 || !this.ctx.state.deprecated)) {
724
+ const data = await this.modelInstance.select(where, {
725
+ field: ['url'],
726
+ });
727
+
728
+ return url.map((u) => data.filter(({ url }) => url === u).length);
729
+ }
730
+ const data = await this.modelInstance.count(where);
731
+
732
+ return data;
733
+ }
769
734
  };
@@ -48,12 +48,22 @@ module.exports = class extends think.Controller {
48
48
 
49
49
  async hook(name, ...args) {
50
50
  const fn = this.config(name);
51
+ const plugins = think.getPluginHook(name);
51
52
 
52
- if (!think.isFunction(fn)) {
53
- return;
53
+ if (think.isFunction(fn)) {
54
+ plugins.unshift(fn);
54
55
  }
55
56
 
56
- return fn.call(this, ...args);
57
+ for(let i = 0; i < plugins.length; i++) {
58
+ if (!think.isFunction(plugins[i])) {
59
+ continue;
60
+ }
61
+
62
+ const resp = await plugins[i].call(this, ...args);
63
+ if (resp) {
64
+ return resp;
65
+ }
66
+ }
57
67
  }
58
68
 
59
69
  __call() {}
@@ -101,4 +101,58 @@ module.exports = {
101
101
 
102
102
  return ua;
103
103
  },
104
+ getLevel(val) {
105
+ const levels = this.config('levels');
106
+ const defaultLevel = 0;
107
+
108
+ if (!val) {
109
+ return defaultLevel;
110
+ }
111
+
112
+ const level = think.findLastIndex(levels, (l) => l <= val);
113
+
114
+ return level === -1 ? defaultLevel : level;
115
+ },
116
+ pluginMap(type, callback) {
117
+ const plugins = think.config('plugins');
118
+ const fns = [];
119
+
120
+ if (!think.isArray(plugins)) {
121
+ return fns;
122
+ }
123
+
124
+
125
+ for (let i = 0; i < plugins.length; i++) {
126
+ const plugin = plugins[i];
127
+
128
+ if (!plugin || !plugin[type]) {
129
+ continue;
130
+ }
131
+
132
+ const res = callback(plugin[type]);
133
+ if (!res) {
134
+ continue;
135
+ }
136
+
137
+ fns.push(res);
138
+ }
139
+
140
+ return fns;
141
+ },
142
+ getPluginMiddlewares() {
143
+ const middlewares = think.pluginMap('middlewares', (middleware) => {
144
+ if (think.isFunction(middleware)) {
145
+ return middleware;
146
+ }
147
+
148
+ if (think.isArray(middleware)) {
149
+ return middleware.filter((m) => think.isFunction(m));
150
+ }
151
+ });
152
+
153
+ return middlewares.flat();
154
+ },
155
+ getPluginHook(hookName) {
156
+ return think.pluginMap('hooks', (hook) => think.isFunction(hook[hookName]) ? hook[hookName] : undefined).filter(v => v);
157
+ }
104
158
  };
@@ -0,0 +1,12 @@
1
+ const compose = require('koa-compose');
2
+
3
+ module.exports = function() {
4
+ return (ctx, next) => {
5
+ const middlewares = think.getPluginMiddlewares();
6
+ if (!think.isArray(middlewares) || !middlewares.length) {
7
+ next();
8
+ }
9
+
10
+ compose(middlewares)(ctx, next);
11
+ };
12
+ }