twikoo-func 1.6.45 → 1.7.1
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 +82 -14
- package/package.json +1 -1
- package/utils/constants.js +2 -1
- package/utils/image.js +35 -0
- package/utils/index.js +89 -25
package/index.js
CHANGED
|
@@ -26,9 +26,11 @@ const {
|
|
|
26
26
|
isQQ,
|
|
27
27
|
addQQMailSuffix,
|
|
28
28
|
getQQAvatar,
|
|
29
|
+
getQQNick,
|
|
29
30
|
getPasswordStatus,
|
|
30
31
|
preCheckSpam,
|
|
31
32
|
checkTurnstileCaptcha,
|
|
33
|
+
checkGeeTestCaptcha,
|
|
32
34
|
getConfig,
|
|
33
35
|
getConfigForAdmin,
|
|
34
36
|
validate
|
|
@@ -137,6 +139,9 @@ exports.main = async (event, context) => {
|
|
|
137
139
|
case 'UPLOAD_IMAGE': // >= 1.5.0
|
|
138
140
|
res = await uploadImage(event, config)
|
|
139
141
|
break
|
|
142
|
+
case 'GET_QQ_NICK': // >= 1.7.0
|
|
143
|
+
res = await qqNickGet(event)
|
|
144
|
+
break
|
|
140
145
|
case 'COMMENT_EXPORT_FOR_ADMIN': // >= 1.6.13
|
|
141
146
|
res = await commentExportForAdmin(event)
|
|
142
147
|
break
|
|
@@ -233,6 +238,7 @@ async function commentGet (event) {
|
|
|
233
238
|
const uid = await auth.getEndUserInfo().userInfo.uid
|
|
234
239
|
const isAdminUser = await isAdmin()
|
|
235
240
|
const limit = parseInt(config.COMMENT_PAGE_SIZE) || 8
|
|
241
|
+
const sort = event.sort || 'newest'
|
|
236
242
|
let more = false
|
|
237
243
|
let condition
|
|
238
244
|
let query
|
|
@@ -254,10 +260,21 @@ async function commentGet (event) {
|
|
|
254
260
|
// 不包含置顶
|
|
255
261
|
condition.top = _.neq(true)
|
|
256
262
|
query = getCommentQuery({ condition, uid, isAdminUser })
|
|
263
|
+
|
|
264
|
+
let orderField = 'created'
|
|
265
|
+
let orderDirection = 'desc'
|
|
266
|
+
if (sort === 'oldest') {
|
|
267
|
+
orderField = 'created'
|
|
268
|
+
orderDirection = 'asc'
|
|
269
|
+
} else if (sort === 'popular') {
|
|
270
|
+
orderField = 'ups'
|
|
271
|
+
orderDirection = 'desc'
|
|
272
|
+
}
|
|
273
|
+
|
|
257
274
|
const main = await db
|
|
258
275
|
.collection('comment')
|
|
259
276
|
.where(query)
|
|
260
|
-
.orderBy(
|
|
277
|
+
.orderBy(orderField, orderDirection)
|
|
261
278
|
// 流式分页,通过多读 1 条的方式,确认是否还有更多评论
|
|
262
279
|
.limit(limit + 1)
|
|
263
280
|
.get()
|
|
@@ -514,7 +531,7 @@ async function bulkSaveComments (comments) {
|
|
|
514
531
|
return batchRes.ids
|
|
515
532
|
}
|
|
516
533
|
|
|
517
|
-
// 点赞 / 取消点赞
|
|
534
|
+
// 点赞 / 反对 / 取消点赞
|
|
518
535
|
async function commentLike (event) {
|
|
519
536
|
const res = {}
|
|
520
537
|
let uid
|
|
@@ -525,25 +542,41 @@ async function commentLike (event) {
|
|
|
525
542
|
res.message = e.message
|
|
526
543
|
return res
|
|
527
544
|
}
|
|
528
|
-
|
|
545
|
+
const type = event.type || 'up'
|
|
546
|
+
res.updated = await like(event.id, uid, type)
|
|
529
547
|
return res
|
|
530
548
|
}
|
|
531
549
|
|
|
532
|
-
// 点赞 /
|
|
533
|
-
async function like (id, uid) {
|
|
550
|
+
// 点赞 / 反对 / 取消
|
|
551
|
+
async function like (id, uid, type) {
|
|
534
552
|
const record = db
|
|
535
553
|
.collection('comment')
|
|
536
554
|
.where({ _id: id })
|
|
537
555
|
const comment = await record.get()
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
556
|
+
const commentData = comment.data[0] || {}
|
|
557
|
+
const ups = commentData.ups || []
|
|
558
|
+
const downs = commentData.downs || []
|
|
559
|
+
|
|
560
|
+
let newUps = [...ups]
|
|
561
|
+
let newDowns = [...downs]
|
|
562
|
+
|
|
563
|
+
if (type === 'up') {
|
|
564
|
+
if (ups.includes(uid)) {
|
|
565
|
+
newUps = ups.filter((item) => item !== uid)
|
|
566
|
+
} else {
|
|
567
|
+
newUps.push(uid)
|
|
568
|
+
newDowns = downs.filter((item) => item !== uid)
|
|
569
|
+
}
|
|
570
|
+
} else if (type === 'down') {
|
|
571
|
+
if (downs.includes(uid)) {
|
|
572
|
+
newDowns = downs.filter((item) => item !== uid)
|
|
573
|
+
} else {
|
|
574
|
+
newDowns.push(uid)
|
|
575
|
+
newUps = ups.filter((item) => item !== uid)
|
|
576
|
+
}
|
|
545
577
|
}
|
|
546
|
-
|
|
578
|
+
|
|
579
|
+
const result = await record.update({ ups: newUps, downs: newDowns })
|
|
547
580
|
return result.updated
|
|
548
581
|
}
|
|
549
582
|
|
|
@@ -685,7 +718,27 @@ async function limitFilter () {
|
|
|
685
718
|
}
|
|
686
719
|
|
|
687
720
|
async function checkCaptcha (comment) {
|
|
688
|
-
|
|
721
|
+
const provider = config.CAPTCHA_PROVIDER
|
|
722
|
+
if ((!provider || provider === 'Turnstile') && config.TURNSTILE_SITE_KEY && config.TURNSTILE_SECRET_KEY) {
|
|
723
|
+
await checkTurnstileCaptcha({
|
|
724
|
+
ip: auth.getClientIP(),
|
|
725
|
+
turnstileToken: comment.turnstileToken,
|
|
726
|
+
turnstileTokenSecretKey: config.TURNSTILE_SECRET_KEY
|
|
727
|
+
})
|
|
728
|
+
}
|
|
729
|
+
if ((!provider || provider === 'Geetest') && config.GEETEST_CAPTCHA_ID && config.GEETEST_CAPTCHA_KEY) {
|
|
730
|
+
await checkGeeTestCaptcha({
|
|
731
|
+
geeTestCaptchaId: config.GEETEST_CAPTCHA_ID,
|
|
732
|
+
geeTestCaptchaKey: config.GEETEST_CAPTCHA_KEY,
|
|
733
|
+
geeTestLotNumber: comment.geeTestLotNumber,
|
|
734
|
+
geeTestCaptchaOutput: comment.geeTestCaptchaOutput,
|
|
735
|
+
geeTestPassToken: comment.geeTestPassToken,
|
|
736
|
+
geeTestGenTime: comment.geeTestGenTime
|
|
737
|
+
})
|
|
738
|
+
} else if (config.TURNSTILE_SITE_KEY) {
|
|
739
|
+
if (!config.TURNSTILE_SECRET_KEY) {
|
|
740
|
+
throw new Error('Turnstile 验证码配置不完整,缺少 TURNSTILE_SECRET_KEY')
|
|
741
|
+
}
|
|
689
742
|
await checkTurnstileCaptcha({
|
|
690
743
|
ip: auth.getClientIP(),
|
|
691
744
|
turnstileToken: comment.turnstileToken,
|
|
@@ -840,6 +893,21 @@ async function getRecentComments (event) {
|
|
|
840
893
|
return res
|
|
841
894
|
}
|
|
842
895
|
|
|
896
|
+
// 获取 QQ 昵称
|
|
897
|
+
async function qqNickGet (event) {
|
|
898
|
+
const res = {}
|
|
899
|
+
try {
|
|
900
|
+
validate(event, ['qq'])
|
|
901
|
+
const nick = await getQQNick(event.qq, config.QQ_API_KEY)
|
|
902
|
+
res.code = RES_CODE.SUCCESS
|
|
903
|
+
res.nick = nick
|
|
904
|
+
} catch (e) {
|
|
905
|
+
res.code = RES_CODE.FAIL
|
|
906
|
+
res.message = e.message
|
|
907
|
+
}
|
|
908
|
+
return res
|
|
909
|
+
}
|
|
910
|
+
|
|
843
911
|
// 修改配置
|
|
844
912
|
async function setConfig (event) {
|
|
845
913
|
const isAdminUser = await isAdmin()
|
package/package.json
CHANGED
package/utils/constants.js
CHANGED
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
|
-
|
|
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,
|
|
@@ -244,6 +249,23 @@ const fn = {
|
|
|
244
249
|
logger.warn('获取 QQ 头像失败:', e)
|
|
245
250
|
}
|
|
246
251
|
},
|
|
252
|
+
async getQQNick (qq, qqApiKey) {
|
|
253
|
+
try {
|
|
254
|
+
const qqNum = qq.replace(/@qq.com/ig, '')
|
|
255
|
+
const headers = {}
|
|
256
|
+
if (qqApiKey) {
|
|
257
|
+
headers.Authorization = `Bearer ${qqApiKey}`
|
|
258
|
+
}
|
|
259
|
+
const result = await axios.get(`https://v1.nsuuu.com/api/qqname?qq=${qqNum}`, { headers })
|
|
260
|
+
if (result.data?.code === 200 && result.data?.data?.nick) {
|
|
261
|
+
return result.data.data.nick
|
|
262
|
+
}
|
|
263
|
+
return null
|
|
264
|
+
} catch (e) {
|
|
265
|
+
logger.warn('获取 QQ 昵称失败:', e)
|
|
266
|
+
return null
|
|
267
|
+
}
|
|
268
|
+
},
|
|
247
269
|
// 判断是否存在管理员密码
|
|
248
270
|
async getPasswordStatus (config, version) {
|
|
249
271
|
return {
|
|
@@ -304,33 +326,75 @@ const fn = {
|
|
|
304
326
|
throw new Error('验证码检测失败: ' + e.message)
|
|
305
327
|
}
|
|
306
328
|
},
|
|
329
|
+
async checkGeeTestCaptcha ({ geeTestCaptchaId, geeTestCaptchaKey, geeTestLotNumber, geeTestCaptchaOutput, geeTestPassToken, geeTestGenTime }) {
|
|
330
|
+
try {
|
|
331
|
+
logger.log('极验验证参数:', { geeTestCaptchaId, geeTestCaptchaKey: geeTestCaptchaKey ? '***' : undefined, geeTestLotNumber })
|
|
332
|
+
const crypto = require('crypto')
|
|
333
|
+
const signToken = crypto
|
|
334
|
+
.createHmac('sha256', geeTestCaptchaKey)
|
|
335
|
+
.update(geeTestLotNumber)
|
|
336
|
+
.digest('hex')
|
|
337
|
+
const params = new URLSearchParams()
|
|
338
|
+
params.append('lot_number', geeTestLotNumber)
|
|
339
|
+
params.append('captcha_output', geeTestCaptchaOutput)
|
|
340
|
+
params.append('pass_token', geeTestPassToken)
|
|
341
|
+
params.append('gen_time', geeTestGenTime)
|
|
342
|
+
params.append('sign_token', signToken)
|
|
343
|
+
logger.log('极验请求参数:', params.toString())
|
|
344
|
+
const url = `https://gcaptcha4.geetest.com/validate?captcha_id=${geeTestCaptchaId}`
|
|
345
|
+
const { data } = await axios.post(url, params.toString(), {
|
|
346
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
347
|
+
})
|
|
348
|
+
logger.log('极验验证码检测结果', JSON.stringify(data))
|
|
349
|
+
if (data.result !== 'success') {
|
|
350
|
+
logger.error('极验验证失败详情:', data)
|
|
351
|
+
throw new Error(data.reason || data.msg || '验证码错误')
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
throw new Error('极验验证码检测失败: ' + e.message)
|
|
355
|
+
}
|
|
356
|
+
},
|
|
307
357
|
async getConfig ({ config, VERSION, isAdmin }) {
|
|
358
|
+
// 构建对外配置,避免在启用某一验证码供应商时泄露另一个供应商的 key
|
|
359
|
+
const baseConfig = {
|
|
360
|
+
VERSION,
|
|
361
|
+
IS_ADMIN: isAdmin,
|
|
362
|
+
SITE_NAME: config.SITE_NAME,
|
|
363
|
+
SITE_URL: config.SITE_URL,
|
|
364
|
+
MASTER_TAG: config.MASTER_TAG,
|
|
365
|
+
COMMENT_BG_IMG: config.COMMENT_BG_IMG,
|
|
366
|
+
GRAVATAR_CDN: config.GRAVATAR_CDN,
|
|
367
|
+
DEFAULT_GRAVATAR: config.DEFAULT_GRAVATAR,
|
|
368
|
+
SHOW_IMAGE: config.SHOW_IMAGE || 'true',
|
|
369
|
+
IMAGE_CDN: config.IMAGE_CDN,
|
|
370
|
+
LIGHTBOX: config.LIGHTBOX || 'false',
|
|
371
|
+
SHOW_EMOTION: config.SHOW_EMOTION || 'true',
|
|
372
|
+
EMOTION_CDN: config.EMOTION_CDN,
|
|
373
|
+
COMMENT_PLACEHOLDER: config.COMMENT_PLACEHOLDER,
|
|
374
|
+
DISPLAYED_FIELDS: config.DISPLAYED_FIELDS,
|
|
375
|
+
REQUIRED_FIELDS: config.REQUIRED_FIELDS,
|
|
376
|
+
HIDE_ADMIN_CRYPT: config.HIDE_ADMIN_CRYPT,
|
|
377
|
+
HIGHLIGHT: config.HIGHLIGHT || 'true',
|
|
378
|
+
HIGHLIGHT_THEME: config.HIGHLIGHT_THEME,
|
|
379
|
+
HIGHLIGHT_PLUGIN: config.HIGHLIGHT_PLUGIN,
|
|
380
|
+
LIMIT_LENGTH: config.LIMIT_LENGTH,
|
|
381
|
+
CAPTCHA_PROVIDER: config.CAPTCHA_PROVIDER,
|
|
382
|
+
QQ_API_KEY: config.QQ_API_KEY
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 仅在明确指定使用 Turnstile 时下发 Turnstile 的 site key
|
|
386
|
+
if (config.CAPTCHA_PROVIDER === 'Turnstile') {
|
|
387
|
+
baseConfig.TURNSTILE_SITE_KEY = config.TURNSTILE_SITE_KEY
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 仅在明确指定使用 Geetest 时下发 Geetest 的 id
|
|
391
|
+
if (config.CAPTCHA_PROVIDER === 'Geetest') {
|
|
392
|
+
baseConfig.GEETEST_CAPTCHA_ID = config.GEETEST_CAPTCHA_ID
|
|
393
|
+
}
|
|
394
|
+
|
|
308
395
|
return {
|
|
309
396
|
code: RES_CODE.SUCCESS,
|
|
310
|
-
config:
|
|
311
|
-
VERSION,
|
|
312
|
-
IS_ADMIN: isAdmin,
|
|
313
|
-
SITE_NAME: config.SITE_NAME,
|
|
314
|
-
SITE_URL: config.SITE_URL,
|
|
315
|
-
MASTER_TAG: config.MASTER_TAG,
|
|
316
|
-
COMMENT_BG_IMG: config.COMMENT_BG_IMG,
|
|
317
|
-
GRAVATAR_CDN: config.GRAVATAR_CDN,
|
|
318
|
-
DEFAULT_GRAVATAR: config.DEFAULT_GRAVATAR,
|
|
319
|
-
SHOW_IMAGE: config.SHOW_IMAGE || 'true',
|
|
320
|
-
IMAGE_CDN: config.IMAGE_CDN,
|
|
321
|
-
LIGHTBOX: config.LIGHTBOX || 'false',
|
|
322
|
-
SHOW_EMOTION: config.SHOW_EMOTION || 'true',
|
|
323
|
-
EMOTION_CDN: config.EMOTION_CDN,
|
|
324
|
-
COMMENT_PLACEHOLDER: config.COMMENT_PLACEHOLDER,
|
|
325
|
-
DISPLAYED_FIELDS: config.DISPLAYED_FIELDS,
|
|
326
|
-
REQUIRED_FIELDS: config.REQUIRED_FIELDS,
|
|
327
|
-
HIDE_ADMIN_CRYPT: config.HIDE_ADMIN_CRYPT,
|
|
328
|
-
HIGHLIGHT: config.HIGHLIGHT || 'true',
|
|
329
|
-
HIGHLIGHT_THEME: config.HIGHLIGHT_THEME,
|
|
330
|
-
HIGHLIGHT_PLUGIN: config.HIGHLIGHT_PLUGIN,
|
|
331
|
-
LIMIT_LENGTH: config.LIMIT_LENGTH,
|
|
332
|
-
TURNSTILE_SITE_KEY: config.TURNSTILE_SITE_KEY
|
|
333
|
-
}
|
|
397
|
+
config: baseConfig
|
|
334
398
|
}
|
|
335
399
|
},
|
|
336
400
|
async getConfigForAdmin ({ config, isAdmin }) {
|