twikoo-func 1.6.44 → 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 +52 -13
- package/package.json +1 -1
- package/utils/constants.js +2 -1
- package/utils/image.js +70 -4
- package/utils/index.js +39 -3
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(
|
|
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
|
-
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
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
package/utils/constants.js
CHANGED
package/utils/image.js
CHANGED
|
@@ -16,11 +16,19 @@ 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' })
|
|
22
|
-
} else if (config.IMAGE_CDN === '
|
|
23
|
-
await fn.
|
|
30
|
+
} else if (config.IMAGE_CDN === 'see') {
|
|
31
|
+
await fn.uploadImageToSee({ photo, fileName, config, res, imageCdn: 'https://s.ee/api/v1/file/upload' })
|
|
24
32
|
} else if (isUrl(config.IMAGE_CDN)) {
|
|
25
33
|
await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN })
|
|
26
34
|
} else if (config.IMAGE_CDN === 'lskypro') {
|
|
@@ -29,6 +37,8 @@ const fn = {
|
|
|
29
37
|
await fn.uploadImageToPicList({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN_URL })
|
|
30
38
|
} else if (config.IMAGE_CDN === 'easyimage') {
|
|
31
39
|
await fn.uploadImageToEasyImage({ photo, fileName, config, res })
|
|
40
|
+
} else if (config.IMAGE_CDN === 'chevereto') {
|
|
41
|
+
await fn.uploadImageToChevereto({ photo, fileName, config, res })
|
|
32
42
|
} else {
|
|
33
43
|
throw new Error('不支持的图片上传服务')
|
|
34
44
|
}
|
|
@@ -39,8 +49,35 @@ const fn = {
|
|
|
39
49
|
}
|
|
40
50
|
return res
|
|
41
51
|
},
|
|
42
|
-
async
|
|
43
|
-
|
|
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
|
+
},
|
|
79
|
+
async uploadImageToSee ({ photo, fileName, config, res, imageCdn }) {
|
|
80
|
+
// S.EE 图床 https://s.ee (原 SM.MS)
|
|
44
81
|
const formData = new FormData()
|
|
45
82
|
formData.append('smfile', fn.base64UrlToReadStream(photo, fileName))
|
|
46
83
|
const uploadResult = await axios.post(imageCdn, formData, {
|
|
@@ -148,6 +185,35 @@ const fn = {
|
|
|
148
185
|
throw new Error(errorMsg)
|
|
149
186
|
}
|
|
150
187
|
},
|
|
188
|
+
async uploadImageToChevereto ({ photo, fileName, config, res }) {
|
|
189
|
+
if (!config.IMAGE_CDN_URL) {
|
|
190
|
+
throw new Error('未配置 Chevereto 站点地址 (IMAGE_CDN_URL)')
|
|
191
|
+
}
|
|
192
|
+
if (!config.IMAGE_CDN_TOKEN) {
|
|
193
|
+
throw new Error('未配置 Chevereto API Key (IMAGE_CDN_TOKEN)')
|
|
194
|
+
}
|
|
195
|
+
const formData = new FormData()
|
|
196
|
+
formData.append('key', config.IMAGE_CDN_TOKEN)
|
|
197
|
+
formData.append('source', fn.base64UrlToReadStream(photo, fileName))
|
|
198
|
+
formData.append('format', 'json')
|
|
199
|
+
const apiUrl = config.IMAGE_CDN_URL.replace(/\/$/, '') + '/api/1/upload'
|
|
200
|
+
const uploadResult = await axios.post(apiUrl, formData, {
|
|
201
|
+
headers: {
|
|
202
|
+
...formData.getHeaders()
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
const data = uploadResult.data
|
|
206
|
+
if (data.status_code === 200 && data.image && data.image.url) {
|
|
207
|
+
res.data = {
|
|
208
|
+
url: data.image.url,
|
|
209
|
+
thumb: data.image.thumb ? data.image.thumb.url : data.image.url,
|
|
210
|
+
del: data.image.delete_url
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
const errMsg = (data.error && data.error.message) || JSON.stringify(data)
|
|
214
|
+
throw new Error(`Chevereto 上传失败: ${errMsg}`)
|
|
215
|
+
}
|
|
216
|
+
},
|
|
151
217
|
base64UrlToReadStream (base64Url, fileName) {
|
|
152
218
|
const base64 = base64Url.split(';base64,').pop()
|
|
153
219
|
const writePath = path.resolve(os.tmpdir(), fileName)
|
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,
|
|
@@ -111,7 +116,8 @@ const fn = {
|
|
|
111
116
|
12: 'Monterey',
|
|
112
117
|
13: 'Ventura',
|
|
113
118
|
14: 'Sonoma',
|
|
114
|
-
15: 'Sequoia'
|
|
119
|
+
15: 'Sequoia',
|
|
120
|
+
16: 'Tahoe'
|
|
115
121
|
}[majorPlatformVersion]
|
|
116
122
|
} else if (os.name === 'Android') {
|
|
117
123
|
const majorPlatformVersion = os.version.split('.')[0]
|
|
@@ -303,6 +309,34 @@ const fn = {
|
|
|
303
309
|
throw new Error('验证码检测失败: ' + e.message)
|
|
304
310
|
}
|
|
305
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
|
+
},
|
|
306
340
|
async getConfig ({ config, VERSION, isAdmin }) {
|
|
307
341
|
return {
|
|
308
342
|
code: RES_CODE.SUCCESS,
|
|
@@ -328,7 +362,9 @@ const fn = {
|
|
|
328
362
|
HIGHLIGHT_THEME: config.HIGHLIGHT_THEME,
|
|
329
363
|
HIGHLIGHT_PLUGIN: config.HIGHLIGHT_PLUGIN,
|
|
330
364
|
LIMIT_LENGTH: config.LIMIT_LENGTH,
|
|
331
|
-
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
|
|
332
368
|
}
|
|
333
369
|
}
|
|
334
370
|
},
|