@waline/vercel 1.36.5 → 1.38.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.
Files changed (41) hide show
  1. package/development.js +1 -0
  2. package/index.js +4 -3
  3. package/package.json +2 -2
  4. package/src/config/adapter.js +8 -5
  5. package/src/config/config.js +10 -5
  6. package/src/controller/article.js +2 -2
  7. package/src/controller/comment.js +24 -21
  8. package/src/controller/oauth.js +7 -4
  9. package/src/controller/rest.js +1 -1
  10. package/src/controller/token.js +3 -1
  11. package/src/controller/user.js +30 -8
  12. package/src/controller/verification.js +3 -2
  13. package/src/extend/think.js +33 -19
  14. package/src/locales/de.json +19 -0
  15. package/src/locales/es.json +19 -0
  16. package/src/locales/fr.json +19 -0
  17. package/src/locales/index.js +18 -2
  18. package/src/locales/jp.json +19 -0
  19. package/src/locales/pt-BR.json +19 -0
  20. package/src/locales/ru.json +19 -0
  21. package/src/locales/vi-VN.json +19 -0
  22. package/src/logic/base.js +26 -24
  23. package/src/logic/comment.js +7 -4
  24. package/src/logic/db.js +1 -1
  25. package/src/logic/token.js +2 -2
  26. package/src/logic/user.js +25 -0
  27. package/src/middleware/dashboard.js +1 -0
  28. package/src/middleware/plugin.js +1 -1
  29. package/src/service/akismet.js +10 -3
  30. package/src/service/avatar.js +1 -1
  31. package/src/service/markdown/highlight.js +1 -1
  32. package/src/service/markdown/utils.js +5 -5
  33. package/src/service/markdown/xss.js +5 -10
  34. package/src/service/notify.js +56 -54
  35. package/src/service/storage/base.js +1 -1
  36. package/src/service/storage/cloudbase.js +9 -7
  37. package/src/service/storage/github.js +24 -18
  38. package/src/service/storage/leancloud.js +33 -26
  39. package/src/service/storage/mongodb.js +10 -6
  40. package/src/service/storage/mysql.js +4 -4
  41. package/src/service/storage/postgresql.js +5 -5
package/development.js CHANGED
@@ -21,6 +21,7 @@ instance.run();
21
21
  let config = {};
22
22
 
23
23
  try {
24
+ // oxlint-disable-next-line node/global-require
24
25
  config = require('./config.js');
25
26
  } catch {
26
27
  // do nothing
package/index.js CHANGED
@@ -4,7 +4,7 @@ const path = require('node:path');
4
4
  const Application = require('thinkjs');
5
5
  const Loader = require('thinkjs/lib/loader');
6
6
 
7
- module.exports = function (configParams = {}) {
7
+ module.exports = function main(configParams = {}) {
8
8
  const { env, ...config } = configParams;
9
9
 
10
10
  const app = new Application({
@@ -20,10 +20,11 @@ module.exports = function (configParams = {}) {
20
20
 
21
21
  loader.loadAll('worker');
22
22
 
23
+ // oxlint-disable-next-line func-names
23
24
  return function (req, res) {
24
- for (const k in config) {
25
+ for (const key in config) {
25
26
  // fix https://github.com/walinejs/waline/issues/2649 with alias model config name
26
- think.config(k === 'model' ? 'customModel' : k, config[k]);
27
+ think.config(key === 'model' ? 'customModel' : key, config[key]);
27
28
  }
28
29
 
29
30
  return think
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waline/vercel",
3
- "version": "1.36.5",
3
+ "version": "1.38.0",
4
4
  "description": "vercel server for waline comment system",
5
5
  "keywords": [
6
6
  "blog",
@@ -29,9 +29,9 @@
29
29
  "@mdit/plugin-sup": "0.22.5-cjs.0",
30
30
  "akismet": "^2.0.7",
31
31
  "dompurify": "^3.3.1",
32
- "dy-node-ip2region": "^1.0.1",
33
32
  "fast-csv": "^5.0.5",
34
33
  "form-data": "^4.0.5",
34
+ "ip2region": "^2.3.0",
35
35
  "jsdom": "^19.0.0",
36
36
  "jsonwebtoken": "^9.0.3",
37
37
  "koa-compose": "^4.1.0",
@@ -6,9 +6,10 @@ const Postgresql = require('think-model-postgresql');
6
6
  let Sqlite;
7
7
 
8
8
  try {
9
+ // oxlint-disable-next-line node/global-require
9
10
  Sqlite = require('think-model-sqlite');
10
11
  } catch (err) {
11
- // eslint-disable-next-line @typescript-eslint/no-extraneous-class
12
+ // oxlint-disable-next-line typescript/no-extraneous-class
12
13
  Sqlite = class {};
13
14
  console.log(err);
14
15
  }
@@ -65,11 +66,11 @@ if (MONGO_AUTHSOURCE) mongoOpt.authSource = MONGO_AUTHSOURCE;
65
66
  if (MONGO_DB) {
66
67
  type = 'mongo';
67
68
  for (const envKeys in process.env) {
68
- if (/MONGO_OPT_/.test(envKeys)) {
69
+ if (envKeys.includes('MONGO_OPT_')) {
69
70
  const key = envKeys
70
71
  .slice(10)
71
72
  .toLocaleLowerCase()
72
- .replace(/_([a-z])/g, (_, b) => b.toUpperCase());
73
+ .replaceAll(/_([a-z])/g, (_, b) => b.toUpperCase());
73
74
 
74
75
  mongoOpt[key] = process.env[envKeys];
75
76
  }
@@ -88,7 +89,9 @@ exports.model = {
88
89
  type,
89
90
  common: {
90
91
  logSql: true,
91
- logger: (msg) => think.logger.info(msg),
92
+ logger: (msg) => {
93
+ think.logger.info(msg);
94
+ },
92
95
  },
93
96
 
94
97
  mongo: {
@@ -114,7 +117,7 @@ exports.model = {
114
117
  connectionLimit: 1,
115
118
  prefix: PG_PREFIX || POSTGRES_PREFIX || 'wl_',
116
119
  ssl:
117
- (PG_SSL || POSTGRES_SSL) == 'true' || POSTGRES_URL?.includes('sslmode=require')
120
+ (PG_SSL || POSTGRES_SSL) === 'true' || POSTGRES_URL?.includes('sslmode=require')
118
121
  ? {
119
122
  rejectUnauthorized: false,
120
123
  }
@@ -48,11 +48,12 @@ const {
48
48
  COMMENT_AUDIT,
49
49
  } = process.env;
50
50
 
51
- let storage = 'leancloud';
52
- let jwtKey = JWT_TOKEN || LEAN_KEY;
51
+ let storage = null;
52
+ let jwtKey = JWT_TOKEN;
53
53
 
54
54
  if (LEAN_KEY) {
55
55
  storage = 'leancloud';
56
+ jwtKey = jwtKey || LEAN_KEY;
56
57
  } else if (MONGO_DB) {
57
58
  storage = 'mongodb';
58
59
  jwtKey = jwtKey || MONGO_PASSWORD;
@@ -75,6 +76,10 @@ if (LEAN_KEY) {
75
76
  jwtKey = jwtKey || TENCENTCLOUD_SECRETKEY || TCB_KEY || TCB_ENV;
76
77
  }
77
78
 
79
+ if (storage === null) {
80
+ throw new Error('No valid storage found. Please check your environment variables.');
81
+ }
82
+
78
83
  if (think.env === 'cloudbase' && storage === 'sqlite') {
79
84
  throw new Error("You can't use SQLite in CloudBase platform.");
80
85
  }
@@ -100,7 +105,7 @@ if (isFalse(MARKDOWN_HIGHLIGHT)) markdown.config.highlight = false;
100
105
  let avatarProxy = '';
101
106
 
102
107
  if (AVATAR_PROXY) {
103
- avatarProxy = !isFalse(AVATAR_PROXY) ? AVATAR_PROXY : '';
108
+ avatarProxy = isFalse(AVATAR_PROXY) ? '' : AVATAR_PROXY;
104
109
  }
105
110
 
106
111
  const oauthUrl = OAUTH_URL || 'https://oauth.lithub.cc';
@@ -111,10 +116,10 @@ module.exports = {
111
116
  jwtKey,
112
117
  forbiddenWords,
113
118
  disallowIPList: [],
114
- secureDomains: SECURE_DOMAINS ? SECURE_DOMAINS.split(/\s*,\s*/) : undefined,
119
+ secureDomains: SECURE_DOMAINS ? SECURE_DOMAINS.split(/\s*,\s*/) : null,
115
120
  disableUserAgent: DISABLE_USERAGENT && !isFalse(DISABLE_USERAGENT),
116
121
  disableRegion: DISABLE_REGION && !isFalse(DISABLE_REGION),
117
- levels: !LEVELS || isFalse(LEVELS) ? false : LEVELS.split(/\s*,\s*/).map((v) => Number(v)),
122
+ levels: !LEVELS || isFalse(LEVELS) ? false : LEVELS.split(/\s*,\s*/).map(Number),
118
123
 
119
124
  audit: COMMENT_AUDIT && !isFalse(COMMENT_AUDIT),
120
125
  avatarProxy,
@@ -11,14 +11,14 @@ module.exports = class extends BaseRest {
11
11
  const { deprecated } = this.ctx.state;
12
12
 
13
13
  // path is required
14
- if (!Array.isArray(path) || !path.length) {
14
+ if (!Array.isArray(path) || path.length === 0) {
15
15
  return this.jsonOrSuccess(0);
16
16
  }
17
17
 
18
18
  const resp = await this.modelInstance.select({ url: ['IN', path] });
19
19
 
20
20
  if (think.isEmpty(resp)) {
21
- const counters = new Array(path.length).fill(
21
+ const counters = Array(path.length).fill(
22
22
  type.length === 1 && deprecated
23
23
  ? 0
24
24
  : type.reduce((o, field) => {
@@ -4,12 +4,13 @@ const { getMarkdownParser } = require('../service/markdown/index.js');
4
4
 
5
5
  const markdownParser = getMarkdownParser();
6
6
 
7
- async function formatCmt(
7
+ // oxlint-disable-next-line max-statements
8
+ const formatCmt = async (
8
9
  { ua, ip, ...comment },
9
10
  users = [],
10
11
  { avatarProxy, deprecated },
11
12
  loginUser,
12
- ) {
13
+ ) => {
13
14
  ua = think.uaParser(ua);
14
15
  if (!think.config('disableUserAgent')) {
15
16
  comment.browser = `${ua.browser.name || ''}${(ua.browser.version || '')
@@ -33,7 +34,7 @@ async function formatCmt(
33
34
 
34
35
  comment.avatar =
35
36
  avatarProxy && !avatarUrl.includes(avatarProxy)
36
- ? avatarProxy + '?url=' + encodeURIComponent(avatarUrl)
37
+ ? `${avatarProxy}?url=${encodeURIComponent(avatarUrl)}`
37
38
  : avatarUrl;
38
39
 
39
40
  const isAdmin = loginUser && loginUser.type === 'administrator';
@@ -67,7 +68,7 @@ async function formatCmt(
67
68
  delete comment.updatedAt;
68
69
 
69
70
  return comment;
70
- }
71
+ };
71
72
 
72
73
  module.exports = class extends BaseRest {
73
74
  constructor(ctx) {
@@ -79,12 +80,12 @@ module.exports = class extends BaseRest {
79
80
  const { type } = this.get();
80
81
 
81
82
  const fnMap = {
82
- recent: this.getRecentCommentList,
83
- count: this.getCommentCount,
84
- list: this.getAdminCommentList,
83
+ recent: this['getRecentCommentList'],
84
+ count: this['getCommentCount'],
85
+ list: this['getAdminCommentList'],
85
86
  };
86
87
 
87
- const fn = fnMap[type] || this.getCommentList;
88
+ const fn = fnMap[type] || this['getCommentList'];
88
89
  const data = await fn.call(this);
89
90
 
90
91
  return this.jsonOrSuccess(data);
@@ -122,7 +123,7 @@ module.exports = class extends BaseRest {
122
123
 
123
124
  if (
124
125
  think.isArray(disallowIPList) &&
125
- disallowIPList.length &&
126
+ disallowIPList.length > 0 &&
126
127
  disallowIPList.includes(data.ip)
127
128
  ) {
128
129
  think.logger.debug(`Comment IP ${data.ip} is in disallowIPList`);
@@ -171,7 +172,9 @@ module.exports = class extends BaseRest {
171
172
  think.logger.debug(`Comment initial status is ${data.status}`);
172
173
 
173
174
  if (data.status === 'approved') {
174
- const spam = await akismet(data, this.ctx.serverURL).catch((err) => console.log(err)); // ignore akismet error
175
+ const spam = await akismet(data, this.ctx.serverURL).catch((err) => {
176
+ console.log(err);
177
+ }); // ignore akismet error
175
178
 
176
179
  if (spam === true) {
177
180
  data.status = 'spam';
@@ -272,7 +275,7 @@ module.exports = class extends BaseRest {
272
275
  async putAction() {
273
276
  const { userInfo } = this.ctx.state;
274
277
  const isAdmin = userInfo.type === 'administrator';
275
- let data = isAdmin ? this.post() : this.post('comment,like');
278
+ const data = isAdmin ? this.post() : this.post('comment,like');
276
279
  let oldData = await this.modelInstance.select({ objectId: this.id });
277
280
 
278
281
  if (think.isEmpty(oldData) || think.isEmpty(data)) {
@@ -473,10 +476,10 @@ module.exports = class extends BaseRest {
473
476
  }
474
477
 
475
478
  const userModel = this.getModel('Users');
476
- const user_ids = Array.from(new Set(comments.map(({ user_id }) => user_id).filter((v) => v)));
479
+ const user_ids = [...new Set(comments.map(({ user_id }) => user_id).filter((v) => v))];
477
480
  let users = [];
478
481
 
479
- if (user_ids.length) {
482
+ if (user_ids.length > 0) {
480
483
  users = await userModel.select(
481
484
  { objectId: ['IN', user_ids] },
482
485
  {
@@ -491,12 +494,12 @@ module.exports = class extends BaseRest {
491
494
  _complex: {},
492
495
  };
493
496
 
494
- if (user_ids.length) {
497
+ if (user_ids.length > 0) {
495
498
  countWhere._complex.user_id = ['IN', user_ids];
496
499
  }
497
- const mails = Array.from(new Set(comments.map(({ mail }) => mail).filter((v) => v)));
500
+ const mails = [...new Set(comments.map(({ mail }) => mail).filter((v) => v))];
498
501
 
499
- if (mails.length) {
502
+ if (mails.length > 0) {
500
503
  countWhere._complex.mail = ['IN', mails];
501
504
  }
502
505
  if (!think.isEmpty(countWhere._complex)) {
@@ -610,11 +613,11 @@ module.exports = class extends BaseRest {
610
613
  });
611
614
 
612
615
  const userModel = this.getModel('Users');
613
- const user_ids = Array.from(new Set(comments.map(({ user_id }) => user_id).filter((v) => v)));
616
+ const user_ids = [...new Set(comments.map(({ user_id }) => user_id).filter((v) => v))];
614
617
 
615
618
  let users = [];
616
619
 
617
- if (user_ids.length) {
620
+ if (user_ids.length > 0) {
618
621
  users = await userModel.select(
619
622
  { objectId: ['IN', user_ids] },
620
623
  {
@@ -679,11 +682,11 @@ module.exports = class extends BaseRest {
679
682
  });
680
683
 
681
684
  const userModel = this.getModel('Users');
682
- const user_ids = Array.from(new Set(comments.map(({ user_id }) => user_id).filter((v) => v)));
685
+ const user_ids = [...new Set(comments.map(({ user_id }) => user_id).filter((v) => v))];
683
686
 
684
687
  let users = [];
685
688
 
686
- if (user_ids.length) {
689
+ if (user_ids.length > 0) {
687
690
  users = await userModel.select(
688
691
  { objectId: ['IN', user_ids] },
689
692
  {
@@ -707,7 +710,7 @@ module.exports = class extends BaseRest {
707
710
  async getCommentCount() {
708
711
  const { url } = this.get();
709
712
  const { userInfo } = this.ctx.state;
710
- const where = Array.isArray(url) && url.length ? { url: ['IN', url] } : {};
713
+ const where = Array.isArray(url) && url.length > 0 ? { url: ['IN', url] } : {};
711
714
 
712
715
  if (think.isEmpty(userInfo)) {
713
716
  where.status = ['NOT IN', ['waiting', 'spam']];
@@ -19,12 +19,13 @@ module.exports = class extends think.Controller {
19
19
  type,
20
20
  });
21
21
 
22
- return this.redirect(
22
+ this.redirect(
23
23
  think.buildUrl(`${oauthUrl}/${type}`, {
24
24
  redirect: redirectUrl,
25
25
  state: this.ctx.state.token || '',
26
26
  }),
27
27
  );
28
+ return;
28
29
  }
29
30
 
30
31
  /**
@@ -64,7 +65,8 @@ module.exports = class extends think.Controller {
64
65
  const token = jwt.sign(userBySocial[0].objectId, this.config('jwtKey'));
65
66
 
66
67
  if (redirect) {
67
- return this.redirect(think.buildUrl(redirect, { token }));
68
+ this.redirect(think.buildUrl(redirect, { token }));
69
+ return;
68
70
  }
69
71
 
70
72
  return this.success();
@@ -84,7 +86,8 @@ module.exports = class extends think.Controller {
84
86
  objectId: current.objectId,
85
87
  });
86
88
 
87
- return this.redirect('/ui/profile');
89
+ this.redirect('/ui/profile');
90
+ return;
88
91
  }
89
92
 
90
93
  // when user has not login, then we create account by the social type!
@@ -108,6 +111,6 @@ module.exports = class extends think.Controller {
108
111
  // and then generate token!
109
112
  const token = jwt.sign(user.objectId, this.config('jwtKey'));
110
113
 
111
- return this.redirect(redirect + (redirect.includes('?') ? '&' : '?') + 'token=' + token);
114
+ this.redirect(redirect + (redirect.includes('?') ? '&' : '?') + 'token=' + token);
112
115
  }
113
116
  };
@@ -21,7 +21,7 @@ module.exports = class extends think.Controller {
21
21
  const filename = this.__filename || __filename;
22
22
  const last = filename.lastIndexOf(path.sep);
23
23
 
24
- return filename.substr(last + 1, filename.length - last - 4);
24
+ return filename.slice(last + 1, filename.length - last - 4);
25
25
  }
26
26
 
27
27
  getId() {
@@ -17,7 +17,9 @@ module.exports = class extends BaseRest {
17
17
  const { email, password, code } = this.post();
18
18
  const user = await this.modelInstance.select({ email });
19
19
 
20
- if (think.isEmpty(user) || /^verify:/i.test(user[0].type)) {
20
+ const isVerifyUser = /^verify:/i.test(user?.[0]?.type);
21
+ const isBannedUser = user?.[0]?.type === 'banned';
22
+ if (think.isEmpty(user) || isVerifyUser || isBannedUser) {
21
23
  return this.fail();
22
24
  }
23
25
 
@@ -1,6 +1,6 @@
1
1
  const BaseRest = require('./rest.js');
2
2
 
3
- module.exports = class extends BaseRest {
3
+ module.exports = class UserController extends BaseRest {
4
4
  constructor(...args) {
5
5
  super(...args);
6
6
  this.modelInstance = this.getModel('Users');
@@ -80,7 +80,7 @@ module.exports = class extends BaseRest {
80
80
 
81
81
  try {
82
82
  const notify = this.service('notify', this);
83
- const apiUrl = think.buildUrl(this.ctx.serverURL + '/verification', {
83
+ const apiUrl = think.buildUrl(`${this.ctx.serverURL}/verification`, {
84
84
  token,
85
85
  email: data.email,
86
86
  });
@@ -179,6 +179,28 @@ module.exports = class extends BaseRest {
179
179
  return this.success();
180
180
  }
181
181
 
182
+ async deleteAction() {
183
+ const users = await this.modelInstance.select({
184
+ objectId: this.id,
185
+ });
186
+
187
+ if (think.isEmpty(users)) {
188
+ return this.fail();
189
+ }
190
+
191
+ const user = users[0];
192
+ const isVerifyUser = /^verify:/i.test(user.type);
193
+
194
+ if (isVerifyUser) {
195
+ await this.modelInstance.delete({ objectId: this.id });
196
+ } else {
197
+ await this.modelInstance.update({ type: 'banned' }, { objectId: this.id });
198
+ }
199
+
200
+ return this.success();
201
+ }
202
+
203
+ // oxlint-disable-next-line max-statements
182
204
  async getUsersListByCount() {
183
205
  const { pageSize } = this.get();
184
206
  const commentModel = this.getModel('Comment');
@@ -196,9 +218,9 @@ module.exports = class extends BaseRest {
196
218
 
197
219
  const userIds = counts.filter(({ user_id }) => user_id).map(({ user_id }) => user_id);
198
220
 
199
- let usersMap = {};
221
+ const usersMap = {};
200
222
 
201
- if (userIds.length) {
223
+ if (userIds.length > 0) {
202
224
  const users = await this.modelInstance.select({
203
225
  objectId: ['IN', userIds],
204
226
  });
@@ -220,7 +242,7 @@ module.exports = class extends BaseRest {
220
242
  let level = 0;
221
243
 
222
244
  if (user.count) {
223
- const _level = think.findLastIndex(this.config('levels'), (l) => l <= user.count);
245
+ const _level = think.findLastIndex(this.config('levels'), (level) => level <= user.count);
224
246
 
225
247
  if (_level !== -1) {
226
248
  level = _level;
@@ -233,7 +255,7 @@ module.exports = class extends BaseRest {
233
255
  const { display_name: nick, url: link, avatar: avatarUrl, label } = users[count.user_id];
234
256
  const avatar =
235
257
  avatarProxy && !avatarUrl.includes(avatarProxy)
236
- ? avatarProxy + '?url=' + encodeURIComponent(avatarUrl)
258
+ ? `${avatarProxy}?url=${encodeURIComponent(avatarUrl)}`
237
259
  : avatarUrl;
238
260
 
239
261
  Object.assign(user, { nick, link, avatar, label });
@@ -246,7 +268,7 @@ module.exports = class extends BaseRest {
246
268
  if (think.isEmpty(comments)) {
247
269
  continue;
248
270
  }
249
- const comment = comments[0];
271
+ const [comment] = comments;
250
272
 
251
273
  if (think.isEmpty(comment)) {
252
274
  continue;
@@ -255,7 +277,7 @@ module.exports = class extends BaseRest {
255
277
  const avatarUrl = await think.service('avatar').stringify(comment);
256
278
  const avatar =
257
279
  avatarProxy && !avatarUrl.includes(avatarProxy)
258
- ? avatarProxy + '?url=' + encodeURIComponent(avatarUrl)
280
+ ? `${avatarProxy}?url=${encodeURIComponent(avatarUrl)}`
259
281
  : avatarUrl;
260
282
 
261
283
  Object.assign(user, { nick, link, avatar });
@@ -21,10 +21,11 @@ module.exports = class extends BaseRest {
21
21
  return this.fail(this.locale('USER_REGISTERED'));
22
22
  }
23
23
 
24
- if (token === match[1] && Date.now() < parseInt(match[2])) {
24
+ if (token === match[1] && Date.now() < Number.parseInt(match[2])) {
25
25
  await this.modelInstance.update({ type: 'guest' }, { email });
26
26
 
27
- return this.redirect('/ui/login');
27
+ this.redirect('/ui/login');
28
+ return;
28
29
  }
29
30
 
30
31
  return this.fail(this.locale('TOKEN_EXPIRED'));
@@ -1,10 +1,24 @@
1
- const ip2region = require('dy-node-ip2region');
2
- const helper = require('think-helper');
1
+ const IP2Region = require('ip2region').default;
3
2
  const parser = require('ua-parser-js');
4
3
 
5
4
  const preventMessage = 'PREVENT_NEXT_PROCESS';
6
5
 
7
- const regionSearch = ip2region.create(process.env.IP2REGION_DB);
6
+ // Cached IP2Region instance using IIFE closure pattern
7
+ // Instance is created on first access and reused for all subsequent calls
8
+ const getIP2RegionInstance = (() => {
9
+ let instance = null;
10
+
11
+ return () => {
12
+ if (!instance) {
13
+ instance = new IP2Region({
14
+ ipv4db: process.env.IP2REGION_DB_V4 || process.env.IP2REGION_DB,
15
+ ipv6db: process.env.IP2REGION_DB_V6,
16
+ });
17
+ }
18
+
19
+ return instance;
20
+ };
21
+ })();
8
22
 
9
23
  const OS_VERSION_MAP = {
10
24
  Windows: {
@@ -34,15 +48,17 @@ module.exports = {
34
48
  },
35
49
  promiseAllQueue(promises, taskNum) {
36
50
  return new Promise((resolve, reject) => {
37
- if (!promises.length) {
38
- return resolve();
51
+ if (promises.length === 0) {
52
+ resolve();
53
+
54
+ return;
39
55
  }
40
56
 
41
57
  const ret = [];
42
58
  let index = 0;
43
59
  let count = 0;
44
60
 
45
- function runTask() {
61
+ const runTask = () => {
46
62
  const idx = index;
47
63
 
48
64
  index += 1;
@@ -59,7 +75,7 @@ module.exports = {
59
75
 
60
76
  return runTask();
61
77
  }, reject);
62
- }
78
+ };
63
79
 
64
80
  for (let i = 0; i < taskNum; i++) {
65
81
  runTask();
@@ -67,19 +83,17 @@ module.exports = {
67
83
  });
68
84
  },
69
85
  async ip2region(ip, { depth = 1 }) {
70
- if (!ip || ip.includes(':')) return '';
86
+ if (!ip) return '';
71
87
 
72
88
  try {
73
- const search = helper.promisify(regionSearch.btreeSearch, regionSearch);
74
- const result = await search(ip);
89
+ const res = getIP2RegionInstance().search(ip);
75
90
 
76
- if (!result) {
91
+ if (!res) {
77
92
  return '';
78
93
  }
79
- const { region } = result;
80
- const [, , province, city, isp] = region.split('|');
81
- const address = Array.from(new Set([province, city, isp].filter((v) => v)));
82
94
 
95
+ const { province, city, isp } = res;
96
+ const address = [...new Set([province, city, isp].filter(Boolean))];
83
97
  return address.slice(0, depth).join(' ');
84
98
  } catch (err) {
85
99
  console.log(err);
@@ -104,7 +118,7 @@ module.exports = {
104
118
  return defaultLevel;
105
119
  }
106
120
 
107
- const level = think.findLastIndex(levels, (l) => l <= val);
121
+ const level = think.findLastIndex(levels, (level) => level <= val);
108
122
 
109
123
  return level === -1 ? defaultLevel : level;
110
124
  },
@@ -139,7 +153,7 @@ module.exports = {
139
153
  }
140
154
 
141
155
  if (think.isArray(middleware)) {
142
- return middleware.filter((m) => think.isFunction(m));
156
+ return middleware.filter((middleware) => think.isFunction(middleware));
143
157
  }
144
158
  });
145
159
 
@@ -147,8 +161,8 @@ module.exports = {
147
161
  },
148
162
  getPluginHook(hookName) {
149
163
  return think
150
- .pluginMap('hooks', (hook) => (think.isFunction(hook[hookName]) ? hook[hookName] : undefined))
151
- .filter((v) => v);
164
+ .pluginMap('hooks', (hook) => (think.isFunction(hook[hookName]) ? hook[hookName] : null))
165
+ .filter(Boolean);
152
166
  },
153
167
  buildUrl(path, query = {}) {
154
168
  const notEmptyQuery = {};
@@ -165,7 +179,7 @@ module.exports = {
165
179
  let destUrl = path;
166
180
 
167
181
  if (destUrl && notEmptyQueryStr) {
168
- destUrl += destUrl.indexOf('?') !== -1 ? '&' : '?';
182
+ destUrl += destUrl.includes('?') ? '&' : '?';
169
183
  }
170
184
  if (notEmptyQueryStr) {
171
185
  destUrl += notEmptyQueryStr;
@@ -0,0 +1,19 @@
1
+ {
2
+ "import data format not support!": "Dateiformat wird nicht unterstützt",
3
+ "USER_EXIST": "Benutzer existiert bereits",
4
+ "USER_NOT_EXIST": "Benutzer existiert nicht",
5
+ "USER_REGISTERED": "Benutzer bereits registriert",
6
+ "TOKEN_EXPIRED": "Token ist abgelaufen",
7
+ "TWO_FACTOR_AUTH_ERROR_DETAIL": "Fehler bei der Zwei-Faktor-Authentifizierung",
8
+ "[{{name | safe}}] Registration Confirm Mail": "【{{name | safe}}】 Registrierungsbestätigung",
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.": "Bitte klicken Sie auf <a href=\"{{url}}\">{{url}}</a>, um die Registrierung zu bestätigen. Der Link ist 1 Stunde gültig. Falls Sie sich nicht registriert haben, ignorieren Sie diese E-Mail.",
10
+ "[{{name | safe}}] Reset Password": "【{{name | safe}}】 Passwort zurücksetzen",
11
+ "Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Bitte klicken Sie auf <a href=\"{{url}}\">{{url}}</a>, um sich anzumelden und Ihr Passwort so schnell wie möglich zu ändern!",
12
+ "Duplicate Content": "Doppelter Inhalt gesendet",
13
+ "Comment too fast": "Sie kommentieren zu schnell, bitte warten Sie einen Moment!",
14
+ "Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Fehler beim Senden der Registrierungsbestätigung. Bitte {%- if isAdmin -%}überprüfen Sie Ihre Mail-Konfiguration{%- else -%}prüfen Sie Ihre E-Mail-Adresse und kontaktieren Sie den Administrator{%- endif -%}.",
15
+ "MAIL_SUBJECT": "{{parent.nick | safe}},Sie haben eine Antwort auf ' {{site.name | safe}} ' erhalten",
16
+ "MAIL_TEMPLATE": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> Ihr Kommentar auf <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> hat eine neue Antwort </h2>{{parent.nick}}, Sie haben kommentiert: <div style='padding:0 12px 0 12px;margin-top:18px'> <div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{parent.comment | safe}}</div><p><strong>{{self.nick}}</strong> hat geantwortet:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Sie können die vollständige Antwort ansehen unter <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>hier</a> und besuchen Sie erneut <a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>.</p><br/> </div></div>",
17
+ "MAIL_SUBJECT_ADMIN": "Neue Kommentare auf {{site.name | safe}}",
18
+ "MAIL_TEMPLATE_ADMIN": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> Ihr Artikel auf <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> hat neue Kommentare </h2> <p><strong>{{self.nick}}</strong> hat geantwortet:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Sie können die Antwort vollständig ansehen unter <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>hier</a></p><br/> </div>"
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "import data format not support!": "Formato de archivo no compatible",
3
+ "USER_EXIST": "El usuario ya existe",
4
+ "USER_NOT_EXIST": "El usuario no existe",
5
+ "USER_REGISTERED": "Usuario ya registrado",
6
+ "TOKEN_EXPIRED": "El token ha expirado",
7
+ "TWO_FACTOR_AUTH_ERROR_DETAIL": "Error de autenticación en dos pasos",
8
+ "[{{name | safe}}] Registration Confirm Mail": "【{{name | safe}}】Correo de confirmación de registro",
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.": "Por favor haga clic en <a href=\"{{url}}\">{{url}}</a> para confirmar el registro. El enlace es válido por 1 hora. Si no se ha registrado, ignore este correo.",
10
+ "[{{name | safe}}] Reset Password": "【{{name | safe}}】Restablecer contraseña",
11
+ "Please click <a href=\"{{url}}\">{{url}}</a> to login and change your password as soon as possible!": "Por favor haga clic en <a href=\"{{url}}\">{{url}}</a> para iniciar sesión y cambiar su contraseña lo antes posible!",
12
+ "Duplicate Content": "Contenido duplicado enviado",
13
+ "Comment too fast": "Comentarios muy rápidos, por favor espere un momento!",
14
+ "Registration confirm mail send failed, please {%- if isAdmin -%}check your mail configuration{%- else -%}check your email address and contact administrator{%- endif -%}.": "Error al enviar el correo de confirmación. Por favor {%- if isAdmin -%}verifique su configuración de correo{%- else -%}verifique su dirección de correo y contacte al administrador{%- endif -%}.",
15
+ "MAIL_SUBJECT": "{{parent.nick | safe}},Su comentario en '{{site.name | safe}}' ha recibido una respuesta",
16
+ "MAIL_TEMPLATE": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> Su comentario en <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> tiene una nueva respuesta </h2>{{parent.nick}}, usted comentó: <div style='padding:0 12px 0 12px;margin-top:18px'> <div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{parent.comment | safe}}</div><p><strong>{{self.nick}}</strong> respondió:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Puedes ver la respuesta completa en <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>aquí</a>, y visita de nuevo <a style='text-decoration:none; color:#12addb' href='{{site.url}}' target='_blank'>{{site.name}}</a>.</p><br/> </div></div>",
17
+ "MAIL_SUBJECT_ADMIN": "Nuevo comentario en {{site.name | safe}}",
18
+ "MAIL_TEMPLATE_ADMIN": "<div style='border-top:2px solid #12ADDB;box-shadow:0 1px 3px #AAAAAA;line-height:180%;padding:0 15px 12px;margin:50px auto;font-size:12px;'> <h2 style='border-bottom:1px solid #DDD;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;'> Su artículo en <a style='text-decoration:none;color: #12ADDB;' href='{{site.url}}' target='_blank'>{{site.name}}</a> tiene nuevos comentarios </h2> <p><strong>{{self.nick}}</strong> dijo:</p><div style='background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;'>{{self.comment | safe}}</div><p>Puedes ver la respuesta completa en <a style='text-decoration:none; color:#12addb' href='{{site.postUrl}}' target='_blank'>aquí</a></p><br/> </div>"
19
+ }