@waline/vercel 1.13.4 → 1.14.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,8 +1,17 @@
1
1
  {
2
2
  "name": "@waline/vercel",
3
- "version": "1.13.4",
3
+ "version": "1.14.0",
4
4
  "description": "vercel server for waline comment system",
5
- "repository": "https://github.com/walinejs/waline",
5
+ "keywords": [
6
+ "waline",
7
+ "vercel",
8
+ "comment",
9
+ "blog"
10
+ ],
11
+ "repository": {
12
+ "url": "https://github.com/walinejs/waline",
13
+ "directory": "packages/server"
14
+ },
6
15
  "license": "MIT",
7
16
  "author": "lizheming <i@imnerd.org>",
8
17
  "dependencies": {
@@ -31,7 +40,7 @@
31
40
  "think-logger3": "^1.3.1",
32
41
  "think-model": "^1.5.4",
33
42
  "think-model-mysql": "^1.1.7",
34
- "think-model-postgresql": "1.1.7",
43
+ "think-model-postgresql": "1.1.6",
35
44
  "think-model-sqlite": "^1.2.3",
36
45
  "think-mongo": "^2.1.2",
37
46
  "think-router-rest": "^1.0.5",
@@ -1,5 +1,6 @@
1
1
  const Model = require('think-model');
2
2
  const Mongo = require('think-mongo');
3
+ const request = require('request-promise-native');
3
4
 
4
5
  module.exports = [
5
6
  Model(think.app),
@@ -12,16 +13,23 @@ module.exports = [
12
13
  return SERVER_URL;
13
14
  }
14
15
 
15
- const { protocol, host, path, controller } = this;
16
- return `${protocol}://${host}${path.slice(0, -controller.length)}`;
16
+ const { protocol, host } = this;
17
+ return `${protocol}://${host}`;
17
18
  },
18
- },
19
- controller: {
20
- fail(...args) {
21
- if (this.ctx.status === 200) {
22
- this.ctx.status = 500;
19
+ async webhook(type, data) {
20
+ const { WEBHOOK } = process.env;
21
+ if (!WEBHOOK) {
22
+ return;
23
23
  }
24
- this.ctx.fail(...args);
24
+
25
+ return request({
26
+ uri: WEBHOOK,
27
+ method: 'POST',
28
+ headers: {
29
+ 'content-type': 'application/json',
30
+ },
31
+ body: JSON.stringify({ type, data }),
32
+ });
25
33
  },
26
34
  },
27
35
  },
@@ -218,7 +218,12 @@ module.exports = class extends BaseRest {
218
218
  };
219
219
  }
220
220
 
221
- const comments = await this.modelInstance.select(where, {
221
+ const totalCount = await this.modelInstance.count(where);
222
+ const pageOffset = Math.max((page - 1) * pageSize, 0);
223
+ let comments = [];
224
+ let rootComments = [];
225
+ let rootCount = 0;
226
+ const selectOptions = {
222
227
  desc: 'insertedAt',
223
228
  field: [
224
229
  'status',
@@ -233,7 +238,54 @@ module.exports = class extends BaseRest {
233
238
  'user_id',
234
239
  'sticky',
235
240
  ],
236
- });
241
+ };
242
+
243
+ /**
244
+ * most of case we have just little comments
245
+ * while if we want get rootComments, rootCount, childComments with pagination
246
+ * we have to query three times from storage service
247
+ * That's so expensive for user, especially in the serverless.
248
+ * so we have a comments length check
249
+ * If you have less than 1000 comments, then we'll get all comments one time
250
+ * then we'll compute rootComment, rootCount, childComments in program to reduce http request query
251
+ *
252
+ * Why we have limit and the limit is 1000?
253
+ * Many serverless storages have fetch data limit, for example LeanCloud is 100, and CloudBase is 1000
254
+ * If we have much commments, We should use more request to fetch all comments
255
+ * If we have 3000 comments, We have to use 30 http request to fetch comments, things go athwart.
256
+ * And Serverless Service like vercel have excute time limit
257
+ * if we have more http requests in a serverless function, it may timeout easily.
258
+ * so we use limit to avoid it.
259
+ */
260
+ if (totalCount < 1000) {
261
+ comments = await this.modelInstance.select(where, selectOptions);
262
+ rootCount = comments.filter(({ rid }) => !rid).length;
263
+ rootComments = [
264
+ ...comments.filter(({ rid, sticky }) => !rid && sticky),
265
+ ...comments.filter(({ rid, sticky }) => !rid && !sticky),
266
+ ].slice(pageOffset, pageOffset + pageSize);
267
+ } else {
268
+ rootComments = await this.modelInstance.select(
269
+ { ...where, rid: undefined },
270
+ {
271
+ ...selectOptions,
272
+ offset: pageOffset,
273
+ limit: pageSize,
274
+ }
275
+ );
276
+ const children = await this.modelInstance.select(
277
+ {
278
+ ...where,
279
+ rid: ['IN', rootComments.map(({ objectId }) => objectId)],
280
+ },
281
+ selectOptions
282
+ );
283
+ comments = [...rootComments, ...children];
284
+ rootCount = await this.modelInstance.count({
285
+ ...where,
286
+ rid: undefined,
287
+ });
288
+ }
237
289
 
238
290
  const userModel = this.service(
239
291
  `storage/${this.config('storage')}`,
@@ -253,18 +305,11 @@ module.exports = class extends BaseRest {
253
305
  );
254
306
  }
255
307
 
256
- const rootCount = comments.filter(({ rid }) => !rid).length;
257
- const pageOffset = Math.max((page - 1) * pageSize, 0);
258
- const rootComments = [
259
- ...comments.filter(({ rid, sticky }) => !rid && sticky),
260
- ...comments.filter(({ rid, sticky }) => !rid && !sticky),
261
- ].slice(pageOffset, pageOffset + pageSize);
262
-
263
308
  return this.json({
264
309
  page,
265
310
  totalPages: Math.ceil(rootCount / pageSize),
266
311
  pageSize,
267
- count: comments.length,
312
+ count: totalCount,
268
313
  data: await Promise.all(
269
314
  rootComments.map(async (comment) => {
270
315
  const cmt = await formatCmt(
@@ -343,7 +388,7 @@ module.exports = class extends BaseRest {
343
388
  'The comment author had post same comment content before'
344
389
  );
345
390
 
346
- return this.fail('Duplicate Content');
391
+ return this.fail(this.locale('Duplicate Content'));
347
392
  }
348
393
 
349
394
  think.logger.debug('Comment duplicate check OK!');
@@ -358,7 +403,7 @@ module.exports = class extends BaseRest {
358
403
 
359
404
  if (!think.isEmpty(recent)) {
360
405
  think.logger.debug(`The author has posted in ${IPQPS} seconeds.`);
361
- return this.fail('Comment too fast!');
406
+ return this.fail(this.locale('Comment too fast!'));
362
407
  }
363
408
 
364
409
  think.logger.debug(`Comment post frequence check OK!`);
@@ -415,21 +460,25 @@ module.exports = class extends BaseRest {
415
460
 
416
461
  think.logger.debug(`Comment have been added to storage.`);
417
462
 
418
- let parrentComment;
419
-
463
+ let parentComment;
420
464
  if (pid) {
421
- parrentComment = await this.modelInstance.select({ objectId: pid });
422
- parrentComment = parrentComment[0];
465
+ parentComment = await this.modelInstance.select({ objectId: pid });
466
+ parentComment = parentComment[0];
423
467
  }
424
468
 
469
+ await this.ctx.webhook('new_comment', {
470
+ comment: { ...resp, rawComment: comment },
471
+ reply: parentComment,
472
+ });
473
+
425
474
  if (comment.status !== 'spam') {
426
475
  const notify = this.service('notify');
427
- await notify.run({ ...resp, rawComment: comment }, parrentComment);
476
+ await notify.run({ ...resp, rawComment: comment }, parentComment);
428
477
  }
429
478
 
430
479
  think.logger.debug(`Comment notify done!`);
431
480
 
432
- await this.hook('postSave', resp, parrentComment);
481
+ await this.hook('postSave', resp, parentComment);
433
482
 
434
483
  think.logger.debug(`Comment post hooks postSave done!`);
435
484
 
@@ -19,10 +19,9 @@ module.exports = class extends BaseRest {
19
19
 
20
20
  for (let i = 0; i < exportData.tables.length; i++) {
21
21
  const tableName = exportData.tables[i];
22
- const model = this.service(
23
- `storage/${this.config('storage')}`,
24
- tableName
25
- );
22
+ const storage = this.config('storage');
23
+ const model = this.service(`storage/${storage}`, tableName);
24
+
26
25
  const data = await model.select({});
27
26
  exportData.data[tableName] = data;
28
27
  }
@@ -36,12 +35,13 @@ module.exports = class extends BaseRest {
36
35
  const jsonText = await readFileAsync(file.path, 'utf-8');
37
36
  const importData = JSON.parse(jsonText);
38
37
  if (!importData || importData.type !== 'waline') {
39
- return this.fail('import data format not support!');
38
+ return this.fail(this.locale('import data format not support!'));
40
39
  }
41
40
 
42
41
  for (let i = 0; i < importData.tables.length; i++) {
43
42
  const tableName = importData.tables[i];
44
- const model = this.model(tableName);
43
+ const storage = this.config('storage');
44
+ const model = this.service(`storage/${storage}`, tableName);
45
45
 
46
46
  // delete all data at first
47
47
  await model.delete({});
@@ -12,7 +12,9 @@ module.exports = class extends think.Controller {
12
12
  <title>Waline Example</title>
13
13
  </head>
14
14
  <body>
15
- <div id="waline" style="max-width: 800px;margin: 0 auto;"></div> <script src="https://cdn.jsdelivr.net/npm/@waline/client/dist/Waline.min.js"></script>
15
+ <div id="waline" style="max-width: 800px;margin: 0 auto;"></div>
16
+ <script src="https://unpkg.com/@waline/client@v2/dist/waline.js"></script>
17
+ <link href='//unpkg.com/@waline/client@v2/dist/waline.css' rel='stylesheet' />
16
18
  <script>
17
19
  console.log(
18
20
  '%c @waline/server %c v${version} ',
@@ -20,7 +22,7 @@ module.exports = class extends think.Controller {
20
22
  'padding:4px;border:1px solid #0078E7;'
21
23
  );
22
24
  const params = new URLSearchParams(location.search.slice(1));
23
- const waline = new Waline({
25
+ const waline = Waline.init({
24
26
  el: '#waline',
25
27
  path: params.get('path') || '/',
26
28
  lang: params.get('lng'),
@@ -47,7 +47,7 @@ module.exports = class extends BaseRest {
47
47
  });
48
48
 
49
49
  if (!verified) {
50
- return this.fail('TWO_FACTOR_AUTH_ERROR_DETAIL');
50
+ return this.fail(this.locale('TWO_FACTOR_AUTH_ERROR_DETAIL'));
51
51
  }
52
52
 
53
53
  const userModel = this.service(
@@ -36,8 +36,13 @@ module.exports = class extends BaseRest {
36
36
  ? `"${SENDER_NAME}" <${SENDER_EMAIL}>`
37
37
  : SMTP_USER,
38
38
  to: user[0].email,
39
- subject: `【${SITE_NAME || 'Waline'}】Reset Password`,
40
- html: `Please click <a href="${profileUrl}">${profileUrl}</a> to login and change your password as soon as possible!`,
39
+ subject: this.locale('[{{name}}] Reset Password', {
40
+ name: SITE_NAME || 'Waline',
41
+ }),
42
+ html: this.locale(
43
+ 'Please click <a href="{{url}}">{{url}}</a> to login and change your password as soon as possible!',
44
+ { url: profileUrl }
45
+ ),
41
46
  });
42
47
 
43
48
  return this.success();
@@ -21,7 +21,7 @@ module.exports = class extends BaseRest {
21
21
  !think.isEmpty(resp) &&
22
22
  ['administrator', 'guest'].includes(resp[0].type)
23
23
  ) {
24
- return this.fail('USER_EXIST');
24
+ return this.fail(this.locale('USER_EXIST'));
25
25
  }
26
26
 
27
27
  const count = await this.modelInstance.count();
@@ -56,21 +56,37 @@ module.exports = class extends BaseRest {
56
56
  return this.success();
57
57
  }
58
58
 
59
- const notify = this.service('notify');
60
- const apiUrl =
61
- this.ctx.serverURL +
62
- '/verification?' +
63
- qs.stringify({ token, email: data.email });
64
-
65
- await notify.transporter.sendMail({
66
- from:
67
- SENDER_EMAIL && SENDER_NAME
68
- ? `"${SENDER_NAME}" <${SENDER_EMAIL}>`
69
- : SMTP_USER,
70
- to: data.email,
71
- subject: `【${SITE_NAME || 'Waline'}】注册确认邮件`,
72
- html: `请点击 ${apiUrl} 确认注册,链接有效时间为 1 个小时。如果不是你在注册,请忽略这封邮件。`,
73
- });
59
+ try {
60
+ const notify = this.service('notify');
61
+ const apiUrl =
62
+ this.ctx.serverURL +
63
+ '/verification?' +
64
+ qs.stringify({ token, email: data.email });
65
+
66
+ await notify.transporter.sendMail({
67
+ from:
68
+ SENDER_EMAIL && SENDER_NAME
69
+ ? `"${SENDER_NAME}" <${SENDER_EMAIL}>`
70
+ : SMTP_USER,
71
+ to: data.email,
72
+ subject: this.locale('[{{name}}] Registration Confirm Mail', {
73
+ name: SITE_NAME || 'Waline',
74
+ }),
75
+ html: this.locale(
76
+ 'Please click <a href="{{url}}">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.',
77
+ { url: apiUrl }
78
+ ),
79
+ });
80
+ } catch (e) {
81
+ console.log(e);
82
+
83
+ return this.fail(
84
+ this.locale(
85
+ 'Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.',
86
+ { isAdmin: think.isEmpty(count) }
87
+ )
88
+ );
89
+ }
74
90
 
75
91
  return this.success({ verify: true });
76
92
  }
@@ -13,13 +13,13 @@ module.exports = class extends BaseRest {
13
13
  const { token, email } = this.get();
14
14
  const users = await this.modelInstance.select({ email });
15
15
  if (think.isEmpty(users)) {
16
- return this.fail('USER_NOT_EXIST');
16
+ return this.fail(this.locale('USER_NOT_EXIST'));
17
17
  }
18
18
 
19
19
  const user = users[0];
20
20
  const match = user.type.match(/^verify:(\d{4}):(\d+)$/i);
21
21
  if (!match) {
22
- return this.fail('USER_REGISTED');
22
+ return this.fail(this.locale('USER_REGISTED'));
23
23
  }
24
24
 
25
25
  if (token === match[1] && Date.now() < parseInt(match[2])) {
@@ -27,6 +27,6 @@ module.exports = class extends BaseRest {
27
27
  return this.redirect('/ui/login');
28
28
  }
29
29
 
30
- return this.fail('TOKEN_EXPIRED');
30
+ return this.fail(this.locale('TOKEN_EXPIRED'));
31
31
  }
32
32
  };
@@ -1,3 +1,6 @@
1
+ const nunjucks = require('nunjucks');
2
+ const locales = require('../locales');
3
+
1
4
  module.exports = {
2
5
  success(...args) {
3
6
  this.ctx.success(...args);
@@ -7,4 +10,12 @@ module.exports = {
7
10
  this.ctx.fail(...args);
8
11
  return think.prevent();
9
12
  },
13
+ locale(message, variables) {
14
+ const { lang } = this.get();
15
+ const locale = locales[(lang || '').toLowerCase()];
16
+ if (locale && locale[message]) {
17
+ message = locale[message];
18
+ }
19
+ return nunjucks.renderString(message, variables);
20
+ },
10
21
  };
@@ -0,0 +1,15 @@
1
+ {
2
+ "import data format not support!": "import data format not support!",
3
+ "USER_EXIST": "USER_EXIST",
4
+ "USER_NOT_EXIST": "USER_NOT_EXIST",
5
+ "USER_REGISTED": "USER_REGISTED",
6
+ "TOKEN_EXPIRED": "TOKEN_EXPIRED",
7
+ "TWO_FACTOR_AUTH_ERROR_DETAIL": "TWO_FACTOR_AUTH_ERROR_DETAIL",
8
+ "[{{name}}] Registration Confirm Mail": "[{{name}}] Registration Confirm Mail",
9
+ "Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.": "Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.",
10
+ "[{{name}}] Reset Password": "[{{name}}] Reset Password",
11
+ "Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!",
12
+ "Duplicate Content": "Duplicate Content",
13
+ "Comment too fast": "Comment too fast",
14
+ "Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}."
15
+ }
@@ -0,0 +1,12 @@
1
+ const en = require('./en.json');
2
+ const zhCN = require('./zh-CN.json');
3
+ const zhTW = require('./zh-TW.json');
4
+
5
+ module.exports = {
6
+ 'zh-cn': zhCN,
7
+ 'zh-tw': zhTW,
8
+ en: en,
9
+ 'en-us': en,
10
+ jp: en,
11
+ 'jp-jp': en,
12
+ };
@@ -0,0 +1,15 @@
1
+ {
2
+ "import data format not support!": "文件格式不支持",
3
+ "USER_EXIST": "用户已存在",
4
+ "USER_NOT_EXIST": "用户不存在",
5
+ "USER_REGISTED": "用户已注册",
6
+ "TOKEN_EXPIRED": "密钥已过期",
7
+ "TWO_FACTOR_AUTH_ERROR_DETAIL": "二步验证失败",
8
+ "[{{name}}] Registration Confirm Mail": "【{{name}}】注册确认邮件",
9
+ "Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.": "请点击 <a href=\"{{url}}\">{{url}}</a> 确认注册,链接有效时间为 1 个小时。如果不是你在注册,请忽略这封邮件。",
10
+ "[{{name}}] Reset Password": "【{{name}}】重置密码",
11
+ "Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "请尽快点击链接 <a href=\"{{url}}\">{{url}}</a> 登录并修改你的密码!",
12
+ "Duplicate Content": "发送的内容之前已经发过",
13
+ "Comment too fast": "评论太快啦,请慢点!",
14
+ "Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "注册确认邮件发送失败,请{%- if isAdmin -%}检查一下网站的邮件相关配置{% else %}确认你的邮箱输入无误并联系管理员{%- endif -%}。"
15
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "import data format not support!": "文件格式不支持",
3
+ "USER_EXIST": "用戶已存在",
4
+ "USER_NOT_EXIST": "用戶不存在",
5
+ "USER_REGISTED": "用戶已註冊",
6
+ "TOKEN_EXPIRED": "密鑰已過期",
7
+ "TWO_FACTOR_AUTH_ERROR_DETAIL": "二步驗證失敗",
8
+ "[{{name}}] Registration Confirm Mail": "『{{name}}』註冊確認郵件",
9
+ "Please click <a href=\"{{url}}\">{{url}}<a/> to confirm registration, the link is valid for 1 hour. If you are not registering, please ignore this email.": "請點擊 <a href=\"{{url}}\">{{url}}</a> 確認註冊,鏈接有效時間為 1 個小時。如果不是你在註冊,請忽略這封郵件。",
10
+ "[{{name}}] Reset Password": "『{{name}}』重置密碼",
11
+ "Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "請盡快點擊鏈接 <a href=\"{{url}}\">{{url}}</a> 登錄並修改你的密碼!",
12
+ "Duplicate Content": "發送的內容之前已經發過",
13
+ "Comment too fast": "評論太快啦,請慢點!",
14
+ "Registeration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "註冊確認郵件發送失敗,{%- if isAdmin -%}檢查一下網站的郵件相關配置{% else %}確認你的郵箱輸入無誤後聯繫管理員{%- endif -%}。"
15
+ }
@@ -14,8 +14,7 @@ module.exports = function () {
14
14
  window.SITE_NAME = ${JSON.stringify(process.env.SITE_NAME)};
15
15
  </script>
16
16
  <script src="${
17
- process.env.WALINE_ADMIN_MODULE_ASSET_URL ||
18
- 'https://cdn.jsdelivr.net/npm/@waline/admin'
17
+ process.env.WALINE_ADMIN_MODULE_ASSET_URL || '//unpkg.com/@waline/admin'
19
18
  }"></script>
20
19
  </body>
21
20
  </html>`;
@@ -154,6 +154,11 @@ module.exports = class extends Base {
154
154
  }
155
155
 
156
156
  async add(data) {
157
+ if (data.objectId) {
158
+ data._id = data.objectId;
159
+ delete data.objectId;
160
+ }
161
+
157
162
  const instance = await this.collection(this.tableName);
158
163
  const { id } = await instance.add(data);
159
164
  return { ...data, objectId: id };
@@ -121,6 +121,11 @@ module.exports = class extends Base {
121
121
  }
122
122
 
123
123
  async add(data) {
124
+ if (data.objectId) {
125
+ data._id = data.objectId;
126
+ delete data.objectId;
127
+ }
128
+
124
129
  const instance = this.mongo(this.tableName);
125
130
  const id = await instance.add(data);
126
131
  return { ...data, objectId: id.toString() };
@@ -57,6 +57,11 @@ module.exports = class extends Base {
57
57
  }
58
58
 
59
59
  async add(data) {
60
+ if (data.objectId) {
61
+ data.id = data.objectId;
62
+ delete data.objectId;
63
+ }
64
+
60
65
  const instance = this.model(this.tableName);
61
66
  const id = await instance.add(data);
62
67
  return { ...data, objectId: id };