twikoo-func 1.6.45 → 1.7.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/index.js CHANGED
@@ -29,6 +29,7 @@ const {
29
29
  getPasswordStatus,
30
30
  preCheckSpam,
31
31
  checkTurnstileCaptcha,
32
+ checkGeeTestCaptcha,
32
33
  getConfig,
33
34
  getConfigForAdmin,
34
35
  validate
@@ -233,6 +234,7 @@ async function commentGet (event) {
233
234
  const uid = await auth.getEndUserInfo().userInfo.uid
234
235
  const isAdminUser = await isAdmin()
235
236
  const limit = parseInt(config.COMMENT_PAGE_SIZE) || 8
237
+ const sort = event.sort || 'newest'
236
238
  let more = false
237
239
  let condition
238
240
  let query
@@ -254,10 +256,21 @@ async function commentGet (event) {
254
256
  // 不包含置顶
255
257
  condition.top = _.neq(true)
256
258
  query = getCommentQuery({ condition, uid, isAdminUser })
259
+
260
+ let orderField = 'created'
261
+ let orderDirection = 'desc'
262
+ if (sort === 'oldest') {
263
+ orderField = 'created'
264
+ orderDirection = 'asc'
265
+ } else if (sort === 'popular') {
266
+ orderField = 'ups'
267
+ orderDirection = 'desc'
268
+ }
269
+
257
270
  const main = await db
258
271
  .collection('comment')
259
272
  .where(query)
260
- .orderBy('created', 'desc')
273
+ .orderBy(orderField, orderDirection)
261
274
  // 流式分页,通过多读 1 条的方式,确认是否还有更多评论
262
275
  .limit(limit + 1)
263
276
  .get()
@@ -514,7 +527,7 @@ async function bulkSaveComments (comments) {
514
527
  return batchRes.ids
515
528
  }
516
529
 
517
- // 点赞 / 取消点赞
530
+ // 点赞 / 反对 / 取消点赞
518
531
  async function commentLike (event) {
519
532
  const res = {}
520
533
  let uid
@@ -525,25 +538,41 @@ async function commentLike (event) {
525
538
  res.message = e.message
526
539
  return res
527
540
  }
528
- res.updated = await like(event.id, uid)
541
+ const type = event.type || 'up'
542
+ res.updated = await like(event.id, uid, type)
529
543
  return res
530
544
  }
531
545
 
532
- // 点赞 / 取消点赞
533
- async function like (id, uid) {
546
+ // 点赞 / 反对 / 取消
547
+ async function like (id, uid, type) {
534
548
  const record = db
535
549
  .collection('comment')
536
550
  .where({ _id: id })
537
551
  const comment = await record.get()
538
- let likes = comment.data[0] && comment.data[0].like ? comment.data[0].like : []
539
- if (likes.findIndex((item) => item === uid) === -1) {
540
- //
541
- likes.push(uid)
542
- } else {
543
- // 取消赞
544
- likes = likes.filter((item) => item !== uid)
552
+ const commentData = comment.data[0] || {}
553
+ const ups = commentData.ups || []
554
+ const downs = commentData.downs || []
555
+
556
+ let newUps = [...ups]
557
+ let newDowns = [...downs]
558
+
559
+ if (type === 'up') {
560
+ if (ups.includes(uid)) {
561
+ newUps = ups.filter((item) => item !== uid)
562
+ } else {
563
+ newUps.push(uid)
564
+ newDowns = downs.filter((item) => item !== uid)
565
+ }
566
+ } else if (type === 'down') {
567
+ if (downs.includes(uid)) {
568
+ newDowns = downs.filter((item) => item !== uid)
569
+ } else {
570
+ newDowns.push(uid)
571
+ newUps = ups.filter((item) => item !== uid)
572
+ }
545
573
  }
546
- const result = await record.update({ like: likes })
574
+
575
+ const result = await record.update({ ups: newUps, downs: newDowns })
547
576
  return result.updated
548
577
  }
549
578
 
@@ -692,6 +721,16 @@ async function checkCaptcha (comment) {
692
721
  turnstileTokenSecretKey: config.TURNSTILE_SECRET_KEY
693
722
  })
694
723
  }
724
+ if (config.GEETEST_CAPTCHA_ID && config.GEETEST_CAPTCHA_KEY) {
725
+ await checkGeeTestCaptcha({
726
+ geeTestCaptchaId: config.GEETEST_CAPTCHA_ID,
727
+ geeTestCaptchaKey: config.GEETEST_CAPTCHA_KEY,
728
+ geeTestLotNumber: comment.geeTestLotNumber,
729
+ geeTestCaptchaOutput: comment.geeTestCaptchaOutput,
730
+ geeTestPassToken: comment.geeTestPassToken,
731
+ geeTestGenTime: comment.geeTestGenTime
732
+ })
733
+ }
695
734
  }
696
735
 
697
736
  async function saveSpamCheckResult (comment, isSpam) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twikoo-func",
3
- "version": "1.6.45",
3
+ "version": "1.7.0",
4
4
  "description": "A simple comment system.",
5
5
  "author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
6
6
  "license": "MIT",
@@ -13,7 +13,8 @@ module.exports = {
13
13
  NEED_LOGIN: 1024,
14
14
  FORBIDDEN: 1403,
15
15
  AKISMET_ERROR: 1030,
16
- UPLOAD_FAILED: 1040
16
+ UPLOAD_FAILED: 1040,
17
+ NSFW_REJECTED: 1041
17
18
  },
18
19
  MAX_REQUEST_TIMES: parseInt(process.env.TWIKOO_THROTTLE) || 250
19
20
  }
package/utils/image.js CHANGED
@@ -16,6 +16,14 @@ const fn = {
16
16
  if (!config.IMAGE_CDN || !config.IMAGE_CDN_TOKEN) {
17
17
  throw new Error('未配置图片上传服务')
18
18
  }
19
+ if (config.NSFW_API_URL) {
20
+ const nsfwResult = await fn.checkNsfw({ photo, config })
21
+ if (nsfwResult.rejected) {
22
+ res.code = RES_CODE.NSFW_REJECTED
23
+ res.err = nsfwResult.message
24
+ return res
25
+ }
26
+ }
19
27
  // tip: qcloud 图床走前端上传,其他图床走后端上传
20
28
  if (config.IMAGE_CDN === '7bu') {
21
29
  await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: 'https://7bu.top' })
@@ -41,6 +49,33 @@ const fn = {
41
49
  }
42
50
  return res
43
51
  },
52
+ async checkNsfw ({ photo, config }) {
53
+ const result = { rejected: false, message: '' }
54
+ try {
55
+ const threshold = parseFloat(config.NSFW_THRESHOLD) || 0.5
56
+ const apiUrl = config.NSFW_API_URL.replace(/\/$/, '')
57
+ const formData = new FormData()
58
+ formData.append('image', fn.base64UrlToReadStream(photo, 'nsfw_check.jpg'))
59
+ const response = await axios.post(`${apiUrl}/classify`, formData, {
60
+ headers: {
61
+ ...formData.getHeaders()
62
+ },
63
+ timeout: 30000
64
+ })
65
+ const scores = response.data
66
+ if (scores && typeof scores === 'object') {
67
+ const nsfwScore = (scores.porn || 0) + (scores.hentai || 0) + (scores.sexy || 0)
68
+ logger.info('NSFW检测分数:', nsfwScore, '阈值:', threshold)
69
+ if (nsfwScore > threshold) {
70
+ result.rejected = true
71
+ result.message = `图片包含不当内容,检测分数 ${nsfwScore.toFixed(3)} 超过阈值 ${threshold}`
72
+ }
73
+ }
74
+ } catch (e) {
75
+ logger.error('NSFW检测失败:', e.message)
76
+ }
77
+ return result
78
+ },
44
79
  async uploadImageToSee ({ photo, fileName, config, res, imageCdn }) {
45
80
  // S.EE 图床 https://s.ee (原 SM.MS)
46
81
  const formData = new FormData()
package/utils/index.js CHANGED
@@ -75,6 +75,8 @@ const fn = {
75
75
  }
76
76
  }
77
77
  const showRegion = !!config.SHOW_REGION && config.SHOW_REGION !== 'false'
78
+ const ups = comment.ups || []
79
+ const downs = comment.downs || []
78
80
  return {
79
81
  id: comment._id.toString(),
80
82
  nick: comment.nick,
@@ -87,7 +89,10 @@ const fn = {
87
89
  ipRegion: showRegion ? fn.getIpRegion({ ip: comment.ip }) : '',
88
90
  master: comment.master,
89
91
  like: comment.like ? comment.like.length : 0,
90
- liked: comment.like ? comment.like.findIndex((item) => item === uid) > -1 : false,
92
+ ups: ups.length,
93
+ downs: downs.length,
94
+ liked: ups.includes(uid),
95
+ disliked: downs.includes(uid),
91
96
  replies: replies,
92
97
  rid: comment.rid,
93
98
  pid: comment.pid,
@@ -304,6 +309,34 @@ const fn = {
304
309
  throw new Error('验证码检测失败: ' + e.message)
305
310
  }
306
311
  },
312
+ async checkGeeTestCaptcha ({ geeTestCaptchaId, geeTestCaptchaKey, geeTestLotNumber, geeTestCaptchaOutput, geeTestPassToken, geeTestGenTime }) {
313
+ try {
314
+ logger.log('极验验证参数:', { geeTestCaptchaId, geeTestCaptchaKey: geeTestCaptchaKey ? '***' : undefined, geeTestLotNumber })
315
+ const crypto = require('crypto')
316
+ const signToken = crypto
317
+ .createHmac('sha256', geeTestCaptchaKey)
318
+ .update(geeTestLotNumber)
319
+ .digest('hex')
320
+ const params = new URLSearchParams()
321
+ params.append('lot_number', geeTestLotNumber)
322
+ params.append('captcha_output', geeTestCaptchaOutput)
323
+ params.append('pass_token', geeTestPassToken)
324
+ params.append('gen_time', geeTestGenTime)
325
+ params.append('sign_token', signToken)
326
+ logger.log('极验请求参数:', params.toString())
327
+ const url = `https://gcaptcha4.geetest.com/validate?captcha_id=${geeTestCaptchaId}`
328
+ const { data } = await axios.post(url, params.toString(), {
329
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
330
+ })
331
+ logger.log('极验验证码检测结果', JSON.stringify(data))
332
+ if (data.result !== 'success') {
333
+ logger.error('极验验证失败详情:', data)
334
+ throw new Error(data.reason || data.msg || '验证码错误')
335
+ }
336
+ } catch (e) {
337
+ throw new Error('极验验证码检测失败: ' + e.message)
338
+ }
339
+ },
307
340
  async getConfig ({ config, VERSION, isAdmin }) {
308
341
  return {
309
342
  code: RES_CODE.SUCCESS,
@@ -329,7 +362,9 @@ const fn = {
329
362
  HIGHLIGHT_THEME: config.HIGHLIGHT_THEME,
330
363
  HIGHLIGHT_PLUGIN: config.HIGHLIGHT_PLUGIN,
331
364
  LIMIT_LENGTH: config.LIMIT_LENGTH,
332
- TURNSTILE_SITE_KEY: config.TURNSTILE_SITE_KEY
365
+ TURNSTILE_SITE_KEY: config.TURNSTILE_SITE_KEY,
366
+ GEETEST_CAPTCHA_ID: config.GEETEST_CAPTCHA_ID,
367
+ QQ_API_KEY: config.QQ_API_KEY
333
368
  }
334
369
  }
335
370
  },