twikoo-func 1.5.11 → 1.6.0-beta.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 +71 -887
- package/package.json +2 -2
- package/utils/constants.js +19 -0
- package/utils/image.js +75 -0
- package/utils/import.js +210 -0
- package/utils/index.js +233 -0
- package/utils/lib.js +33 -0
- package/utils/notify.js +249 -0
- package/utils/spam.js +59 -0
package/index.js
CHANGED
|
@@ -4,25 +4,41 @@
|
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// 三方依赖 / 3rd party dependencies
|
|
8
7
|
const { version: VERSION } = require('./package.json')
|
|
9
8
|
const tcb = require('@cloudbase/node-sdk') // 云开发 SDK
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
9
|
+
const {
|
|
10
|
+
$,
|
|
11
|
+
JSDOM,
|
|
12
|
+
createDOMPurify,
|
|
13
|
+
md5,
|
|
14
|
+
xml2js
|
|
15
|
+
} = require('./utils/lib')
|
|
16
|
+
const {
|
|
17
|
+
getFuncVersion,
|
|
18
|
+
getUrlQuery,
|
|
19
|
+
getUrlsQuery,
|
|
20
|
+
parseComment,
|
|
21
|
+
parseCommentForAdmin,
|
|
22
|
+
getAvatar,
|
|
23
|
+
isQQ,
|
|
24
|
+
addQQMailSuffix,
|
|
25
|
+
getQQAvatar,
|
|
26
|
+
getPasswordStatus,
|
|
27
|
+
preCheckSpam,
|
|
28
|
+
getConfig,
|
|
29
|
+
getConfigForAdmin,
|
|
30
|
+
validate
|
|
31
|
+
} = require('./utils')
|
|
32
|
+
const {
|
|
33
|
+
jsonParse,
|
|
34
|
+
commentImportValine,
|
|
35
|
+
commentImportDisqus,
|
|
36
|
+
commentImportArtalk,
|
|
37
|
+
commentImportTwikoo
|
|
38
|
+
} = require('./utils/import')
|
|
39
|
+
const { postCheckSpam } = require('./utils/spam')
|
|
40
|
+
const { sendNotice, emailTest } = require('./utils/notify')
|
|
41
|
+
const { uploadImage } = require('./utils/image')
|
|
26
42
|
|
|
27
43
|
// 云函数 SDK / tencent cloudbase sdk
|
|
28
44
|
const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })
|
|
@@ -34,38 +50,19 @@ const _ = db.command
|
|
|
34
50
|
const window = new JSDOM('').window
|
|
35
51
|
const DOMPurify = createDOMPurify(window)
|
|
36
52
|
|
|
37
|
-
// 初始化 IP 属地
|
|
38
|
-
const ipRegionSearcher = ipToRegion.create()
|
|
39
|
-
|
|
40
53
|
// 常量 / constants
|
|
41
|
-
const RES_CODE =
|
|
42
|
-
SUCCESS: 0,
|
|
43
|
-
FAIL: 1000,
|
|
44
|
-
EVENT_NOT_EXIST: 1001,
|
|
45
|
-
PASS_EXIST: 1010,
|
|
46
|
-
CONFIG_NOT_EXIST: 1020,
|
|
47
|
-
CREDENTIALS_NOT_EXIST: 1021,
|
|
48
|
-
CREDENTIALS_INVALID: 1025,
|
|
49
|
-
PASS_NOT_EXIST: 1022,
|
|
50
|
-
PASS_NOT_MATCH: 1023,
|
|
51
|
-
NEED_LOGIN: 1024,
|
|
52
|
-
FORBIDDEN: 1403,
|
|
53
|
-
AKISMET_ERROR: 1030,
|
|
54
|
-
UPLOAD_FAILED: 1040
|
|
55
|
-
}
|
|
54
|
+
const { RES_CODE, MAX_REQUEST_TIMES } = require('./utils/constants')
|
|
56
55
|
const ADMIN_USER_ID = 'admin'
|
|
57
|
-
const MAX_REQUEST_TIMES = parseInt(process.env.TWIKOO_THROTTLE) || 250
|
|
58
56
|
|
|
59
57
|
// 全局变量 / variables
|
|
60
58
|
// 警告:全局定义的变量,会被云函数缓存,请慎重定义全局变量
|
|
61
59
|
// 参考 https://docs.cloudbase.net/cloud-function/deep-principle.html 中的 “实例复用”
|
|
62
60
|
let config
|
|
63
|
-
let transporter
|
|
64
61
|
const requestTimes = {}
|
|
65
62
|
|
|
66
63
|
// 云函数入口点 / entry point
|
|
67
64
|
exports.main = async (event, context) => {
|
|
68
|
-
console.log('
|
|
65
|
+
console.log('请求 IP:', auth.getClientIP())
|
|
69
66
|
console.log('请求方法:', event.event)
|
|
70
67
|
console.log('请求参数:', event)
|
|
71
68
|
let res = {}
|
|
@@ -74,7 +71,7 @@ exports.main = async (event, context) => {
|
|
|
74
71
|
await readConfig()
|
|
75
72
|
switch (event.event) {
|
|
76
73
|
case 'GET_FUNC_VERSION':
|
|
77
|
-
res = getFuncVersion()
|
|
74
|
+
res = getFuncVersion({ VERSION })
|
|
78
75
|
break
|
|
79
76
|
case 'COMMENT_GET':
|
|
80
77
|
res = await commentGet(event)
|
|
@@ -104,16 +101,16 @@ exports.main = async (event, context) => {
|
|
|
104
101
|
res = await counterGet(event)
|
|
105
102
|
break
|
|
106
103
|
case 'GET_PASSWORD_STATUS':
|
|
107
|
-
res = await getPasswordStatus()
|
|
104
|
+
res = await getPasswordStatus(config, VERSION)
|
|
108
105
|
break
|
|
109
106
|
case 'SET_PASSWORD':
|
|
110
107
|
res = await setPassword(event)
|
|
111
108
|
break
|
|
112
109
|
case 'GET_CONFIG':
|
|
113
|
-
res = getConfig()
|
|
110
|
+
res = getConfig({ config, VERSION, isAdmin: await isAdmin() })
|
|
114
111
|
break
|
|
115
112
|
case 'GET_CONFIG_FOR_ADMIN':
|
|
116
|
-
res = await getConfigForAdmin()
|
|
113
|
+
res = await getConfigForAdmin({ config, isAdmin: await isAdmin() })
|
|
117
114
|
break
|
|
118
115
|
case 'SET_CONFIG':
|
|
119
116
|
res = await setConfig(event)
|
|
@@ -128,10 +125,10 @@ exports.main = async (event, context) => {
|
|
|
128
125
|
res = await getRecentComments(event)
|
|
129
126
|
break
|
|
130
127
|
case 'EMAIL_TEST': // >= 1.4.6
|
|
131
|
-
res = await emailTest(event)
|
|
128
|
+
res = await emailTest(event, config, await isAdmin())
|
|
132
129
|
break
|
|
133
130
|
case 'UPLOAD_IMAGE': // >= 1.5.0
|
|
134
|
-
res = await uploadImage(event)
|
|
131
|
+
res = await uploadImage(event, config)
|
|
135
132
|
break
|
|
136
133
|
default:
|
|
137
134
|
if (event.event) {
|
|
@@ -153,24 +150,6 @@ exports.main = async (event, context) => {
|
|
|
153
150
|
return res
|
|
154
151
|
}
|
|
155
152
|
|
|
156
|
-
// 获取 Twikoo 云函数版本
|
|
157
|
-
function getFuncVersion () {
|
|
158
|
-
return {
|
|
159
|
-
code: RES_CODE.SUCCESS,
|
|
160
|
-
version: VERSION
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// 判断是否存在管理员密码
|
|
165
|
-
async function getPasswordStatus () {
|
|
166
|
-
return {
|
|
167
|
-
code: RES_CODE.SUCCESS,
|
|
168
|
-
status: !!config.ADMIN_PASS,
|
|
169
|
-
credentials: !!config.CREDENTIALS,
|
|
170
|
-
version: VERSION
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
153
|
// 写入管理密码
|
|
175
154
|
async function setPassword (event) {
|
|
176
155
|
const isAdminUser = await isAdmin()
|
|
@@ -305,7 +284,7 @@ async function commentGet (event) {
|
|
|
305
284
|
.collection('comment')
|
|
306
285
|
.where(query)
|
|
307
286
|
.get()
|
|
308
|
-
res.data = parseComment([...main.data, ...reply.data], uid)
|
|
287
|
+
res.data = parseComment([...main.data, ...reply.data], uid, config)
|
|
309
288
|
res.more = more
|
|
310
289
|
res.count = count.total
|
|
311
290
|
} catch (e) {
|
|
@@ -322,72 +301,6 @@ function getCommentQuery ({ condition, uid, isAdminUser }) {
|
|
|
322
301
|
)
|
|
323
302
|
}
|
|
324
303
|
|
|
325
|
-
// 同时查询 /path 和 /path/ 的评论
|
|
326
|
-
function getUrlQuery (url) {
|
|
327
|
-
const variantUrl = url[url.length - 1] === '/' ? url.substring(0, url.length - 1) : `${url}/`
|
|
328
|
-
return [url, variantUrl]
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// 筛除隐私字段,拼接回复列表
|
|
332
|
-
function parseComment (comments, uid) {
|
|
333
|
-
const result = []
|
|
334
|
-
for (const comment of comments) {
|
|
335
|
-
if (!comment.rid) {
|
|
336
|
-
const replies = comments
|
|
337
|
-
.filter((item) => item.rid === comment._id)
|
|
338
|
-
.map((item) => toCommentDto(item, uid, [], comments))
|
|
339
|
-
.sort((a, b) => a.created - b.created)
|
|
340
|
-
result.push(toCommentDto(comment, uid, replies))
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
return result
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// 将评论记录转换为前端需要的格式
|
|
347
|
-
function toCommentDto (comment, uid, replies = [], comments = []) {
|
|
348
|
-
let displayOs = ''
|
|
349
|
-
let displayBrowser = ''
|
|
350
|
-
if (config.SHOW_UA !== 'false') {
|
|
351
|
-
try {
|
|
352
|
-
const ua = bowser.getParser(comment.ua)
|
|
353
|
-
const os = ua.getOS()
|
|
354
|
-
displayOs = [os.name, os.versionName ? os.versionName : os.version].join(' ')
|
|
355
|
-
displayBrowser = [ua.getBrowserName(), ua.getBrowserVersion()].join(' ')
|
|
356
|
-
} catch (e) {
|
|
357
|
-
console.log('bowser 错误:', e)
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
const showRegion = !!config.SHOW_REGION && config.SHOW_REGION !== 'false'
|
|
361
|
-
return {
|
|
362
|
-
id: comment._id,
|
|
363
|
-
nick: comment.nick,
|
|
364
|
-
avatar: comment.avatar,
|
|
365
|
-
mailMd5: comment.mailMd5 || md5(comment.mail),
|
|
366
|
-
link: comment.link,
|
|
367
|
-
comment: comment.comment,
|
|
368
|
-
os: displayOs,
|
|
369
|
-
browser: displayBrowser,
|
|
370
|
-
ipRegion: showRegion ? getIpRegion({ ip: comment.ip }) : '',
|
|
371
|
-
master: comment.master,
|
|
372
|
-
like: comment.like ? comment.like.length : 0,
|
|
373
|
-
liked: comment.like ? comment.like.findIndex((item) => item === uid) > -1 : false,
|
|
374
|
-
replies: replies,
|
|
375
|
-
rid: comment.rid,
|
|
376
|
-
pid: comment.pid,
|
|
377
|
-
ruser: ruser(comment.pid, comments),
|
|
378
|
-
top: comment.top,
|
|
379
|
-
isSpam: comment.isSpam,
|
|
380
|
-
created: comment.created,
|
|
381
|
-
updated: comment.updated
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// 获取回复人昵称 / Get replied user nick name
|
|
386
|
-
function ruser (pid, comments = []) {
|
|
387
|
-
const comment = comments.find((item) => item._id === pid)
|
|
388
|
-
return comment ? comment.nick : null
|
|
389
|
-
}
|
|
390
|
-
|
|
391
304
|
// 管理员读取评论
|
|
392
305
|
async function commentGetForAdmin (event) {
|
|
393
306
|
const res = {}
|
|
@@ -446,13 +359,6 @@ function getCommentSearchCondition (event) {
|
|
|
446
359
|
return condition
|
|
447
360
|
}
|
|
448
361
|
|
|
449
|
-
function parseCommentForAdmin (comments) {
|
|
450
|
-
for (const comment of comments) {
|
|
451
|
-
comment.ipRegion = getIpRegion({ ip: comment.ip, detail: true })
|
|
452
|
-
}
|
|
453
|
-
return comments
|
|
454
|
-
}
|
|
455
|
-
|
|
456
362
|
// 管理员修改评论
|
|
457
363
|
async function commentSetForAdmin (event) {
|
|
458
364
|
const res = {}
|
|
@@ -506,30 +412,33 @@ async function commentImportForAdmin (event) {
|
|
|
506
412
|
try {
|
|
507
413
|
validate(event, ['source', 'fileId'])
|
|
508
414
|
log(`开始导入 ${event.source}`)
|
|
415
|
+
let comments
|
|
509
416
|
switch (event.source) {
|
|
510
417
|
case 'valine': {
|
|
511
418
|
const valineDb = await readFile(event.fileId, 'json', log)
|
|
512
|
-
await commentImportValine(valineDb, log)
|
|
419
|
+
comments = await commentImportValine(valineDb, log)
|
|
513
420
|
break
|
|
514
421
|
}
|
|
515
422
|
case 'disqus': {
|
|
516
423
|
const disqusDb = await readFile(event.fileId, 'xml', log)
|
|
517
|
-
await commentImportDisqus(disqusDb, log)
|
|
424
|
+
comments = await commentImportDisqus(disqusDb, log)
|
|
518
425
|
break
|
|
519
426
|
}
|
|
520
427
|
case 'artalk': {
|
|
521
428
|
const artalkDb = await readFile(event.fileId, 'json', log)
|
|
522
|
-
await commentImportArtalk(artalkDb, log)
|
|
429
|
+
comments = await commentImportArtalk(artalkDb, log)
|
|
523
430
|
break
|
|
524
431
|
}
|
|
525
432
|
case 'twikoo': {
|
|
526
433
|
const twikooDb = await readFile(event.fileId, 'json', log)
|
|
527
|
-
await commentImportTwikoo(twikooDb, log)
|
|
434
|
+
comments = await commentImportTwikoo(twikooDb, log)
|
|
528
435
|
break
|
|
529
436
|
}
|
|
530
437
|
default:
|
|
531
438
|
throw new Error(`不支持 ${event.source} 的导入,请更新 Twikoo 云函数至最新版本`)
|
|
532
439
|
}
|
|
440
|
+
const ids = await bulkSaveComments(comments)
|
|
441
|
+
log(`导入成功 ${ids.length} 条评论`)
|
|
533
442
|
// 删除导入完成的文件
|
|
534
443
|
await app.deleteFile({ fileList: [event.fileId] })
|
|
535
444
|
} catch (e) {
|
|
@@ -565,230 +474,6 @@ async function readFile (fileId, type, log) {
|
|
|
565
474
|
}
|
|
566
475
|
}
|
|
567
476
|
|
|
568
|
-
// 兼容 Leancloud 两种 JSON 导出格式
|
|
569
|
-
function jsonParse (content) {
|
|
570
|
-
try {
|
|
571
|
-
return JSON.parse(content)
|
|
572
|
-
} catch (e1) {
|
|
573
|
-
const results = []
|
|
574
|
-
const lines = content.split('\n')
|
|
575
|
-
for (const line of lines) {
|
|
576
|
-
// 逐行 JSON.parse
|
|
577
|
-
try {
|
|
578
|
-
results.push(JSON.parse(line))
|
|
579
|
-
} catch (e2) {}
|
|
580
|
-
}
|
|
581
|
-
return { results }
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Valine 导入
|
|
586
|
-
async function commentImportValine (valineDb, log) {
|
|
587
|
-
let arr
|
|
588
|
-
if (valineDb instanceof Array) {
|
|
589
|
-
arr = valineDb
|
|
590
|
-
} else if (valineDb && valineDb.results) {
|
|
591
|
-
arr = valineDb.results
|
|
592
|
-
}
|
|
593
|
-
if (!arr) {
|
|
594
|
-
log('Valine 评论文件格式有误')
|
|
595
|
-
return
|
|
596
|
-
}
|
|
597
|
-
const comments = []
|
|
598
|
-
log(`共 ${arr.length} 条评论`)
|
|
599
|
-
for (const comment of arr) {
|
|
600
|
-
try {
|
|
601
|
-
const parsed = {
|
|
602
|
-
_id: comment.objectId,
|
|
603
|
-
nick: comment.nick,
|
|
604
|
-
ip: comment.ip,
|
|
605
|
-
mail: comment.mail,
|
|
606
|
-
mailMd5: comment.mailMd5,
|
|
607
|
-
isSpam: comment.isSpam,
|
|
608
|
-
ua: comment.ua || '',
|
|
609
|
-
link: comment.link,
|
|
610
|
-
pid: comment.pid,
|
|
611
|
-
rid: comment.rid,
|
|
612
|
-
master: false,
|
|
613
|
-
comment: comment.comment,
|
|
614
|
-
url: comment.url,
|
|
615
|
-
created: new Date(comment.createdAt).getTime(),
|
|
616
|
-
updated: new Date(comment.updatedAt).getTime()
|
|
617
|
-
}
|
|
618
|
-
comments.push(parsed)
|
|
619
|
-
log(`${comment.objectId} 解析成功`)
|
|
620
|
-
} catch (e) {
|
|
621
|
-
log(`${comment.objectId} 解析失败:${e.message}`)
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
log(`解析成功 ${comments.length} 条评论`)
|
|
625
|
-
const ids = await bulkSaveComments(comments)
|
|
626
|
-
log(`导入成功 ${ids.length} 条评论`)
|
|
627
|
-
return comments
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Disqus 导入
|
|
631
|
-
async function commentImportDisqus (disqusDb, log) {
|
|
632
|
-
if (!disqusDb || !disqusDb.disqus || !disqusDb.disqus.thread || !disqusDb.disqus.post) {
|
|
633
|
-
log('Disqus 评论文件格式有误')
|
|
634
|
-
return
|
|
635
|
-
}
|
|
636
|
-
const comments = []
|
|
637
|
-
const getParent = (post) => {
|
|
638
|
-
return post.parent ? disqusDb.disqus.post.find((item) => item.$['dsq:id'] === post.parent[0].$['dsq:id']) : null
|
|
639
|
-
}
|
|
640
|
-
let threads = []
|
|
641
|
-
try {
|
|
642
|
-
threads = disqusDb.disqus.thread.map((thread) => {
|
|
643
|
-
return {
|
|
644
|
-
id: thread.$['dsq:id'],
|
|
645
|
-
title: thread.title[0],
|
|
646
|
-
url: thread.id[0],
|
|
647
|
-
href: thread.link[0]
|
|
648
|
-
}
|
|
649
|
-
})
|
|
650
|
-
} catch (e) {
|
|
651
|
-
log(`无法读取 thread:${e.message}`)
|
|
652
|
-
return
|
|
653
|
-
}
|
|
654
|
-
log(`共 ${disqusDb.disqus.post.length} 条评论`)
|
|
655
|
-
for (const post of disqusDb.disqus.post) {
|
|
656
|
-
try {
|
|
657
|
-
const threadId = post.thread[0].$['dsq:id']
|
|
658
|
-
const thread = threads.find((item) => item.id === threadId)
|
|
659
|
-
const parent = getParent(post)
|
|
660
|
-
let root
|
|
661
|
-
if (parent) {
|
|
662
|
-
let grandParent = parent
|
|
663
|
-
while (true) {
|
|
664
|
-
if (grandParent) root = grandParent
|
|
665
|
-
else break
|
|
666
|
-
grandParent = getParent(grandParent)
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
comments.push({
|
|
670
|
-
_id: post.$['dsq:id'],
|
|
671
|
-
nick: post.author[0].name[0],
|
|
672
|
-
mail: '',
|
|
673
|
-
link: '',
|
|
674
|
-
url: thread.url
|
|
675
|
-
? thread.url.indexOf('http') === 0
|
|
676
|
-
? getRelativeUrl(thread.url)
|
|
677
|
-
: thread.url
|
|
678
|
-
: getRelativeUrl(thread.href),
|
|
679
|
-
href: thread.href,
|
|
680
|
-
comment: post.message[0],
|
|
681
|
-
ua: '',
|
|
682
|
-
ip: '',
|
|
683
|
-
isSpam: post.isSpam[0] === 'true' || post.isDeleted[0] === 'true',
|
|
684
|
-
master: false,
|
|
685
|
-
pid: parent ? parent.$['dsq:id'] : null,
|
|
686
|
-
rid: root ? root.$['dsq:id'] : null,
|
|
687
|
-
created: new Date(post.createdAt[0]).getTime(),
|
|
688
|
-
updated: Date.now()
|
|
689
|
-
})
|
|
690
|
-
log(`${post.$['dsq:id']} 解析成功`)
|
|
691
|
-
} catch (e) {
|
|
692
|
-
log(`${post.$['dsq:id']} 解析失败:${e.message}`)
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
log(`解析成功 ${comments.length} 条评论`)
|
|
696
|
-
const ids = await bulkSaveComments(comments)
|
|
697
|
-
log(`导入成功 ${ids.length} 条评论`)
|
|
698
|
-
return comments
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function getRelativeUrl (url) {
|
|
702
|
-
let x = url.indexOf('/')
|
|
703
|
-
for (let i = 0; i < 2; i++) {
|
|
704
|
-
x = url.indexOf('/', x + 1)
|
|
705
|
-
}
|
|
706
|
-
return url.substring(x)
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Artalk 导入
|
|
710
|
-
async function commentImportArtalk (artalkDb, log) {
|
|
711
|
-
const comments = []
|
|
712
|
-
if (!artalkDb || !artalkDb.length) {
|
|
713
|
-
log('Artalk 评论文件格式有误')
|
|
714
|
-
return
|
|
715
|
-
}
|
|
716
|
-
marked.setOptions({
|
|
717
|
-
renderer: new marked.Renderer(),
|
|
718
|
-
gfm: true,
|
|
719
|
-
tables: true,
|
|
720
|
-
breaks: true,
|
|
721
|
-
pedantic: false,
|
|
722
|
-
sanitize: true,
|
|
723
|
-
smartLists: true,
|
|
724
|
-
smartypants: true
|
|
725
|
-
})
|
|
726
|
-
log(`共 ${artalkDb.length} 条评论`)
|
|
727
|
-
for (const comment of artalkDb) {
|
|
728
|
-
try {
|
|
729
|
-
const parsed = {
|
|
730
|
-
_id: `artalk${comment.id}`,
|
|
731
|
-
nick: comment.nick,
|
|
732
|
-
ip: comment.ip,
|
|
733
|
-
mail: comment.email,
|
|
734
|
-
mailMd5: md5(comment.email),
|
|
735
|
-
isSpam: false,
|
|
736
|
-
ua: comment.ua || '',
|
|
737
|
-
link: comment.link,
|
|
738
|
-
pid: comment.rid ? `artalk${comment.rid}` : '',
|
|
739
|
-
rid: comment.rid ? `artalk${comment.rid}` : '',
|
|
740
|
-
master: false,
|
|
741
|
-
comment: DOMPurify.sanitize(marked(comment.content)),
|
|
742
|
-
url: getRelativeUrl(comment.page_key),
|
|
743
|
-
href: comment.page_key,
|
|
744
|
-
created: new Date(comment.date).getTime(),
|
|
745
|
-
updated: Date.now()
|
|
746
|
-
}
|
|
747
|
-
comments.push(parsed)
|
|
748
|
-
log(`${comment.id} 解析成功`)
|
|
749
|
-
} catch (e) {
|
|
750
|
-
log(`${comment.id} 解析失败:${e.message}`)
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
log(`解析成功 ${comments.length} 条评论`)
|
|
754
|
-
const ids = await bulkSaveComments(comments)
|
|
755
|
-
log(`导入成功 ${ids.length} 条评论`)
|
|
756
|
-
return comments
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
// Twikoo 导入
|
|
760
|
-
async function commentImportTwikoo (twikooDb, log) {
|
|
761
|
-
let arr
|
|
762
|
-
if (twikooDb instanceof Array) {
|
|
763
|
-
arr = twikooDb
|
|
764
|
-
} else if (twikooDb && twikooDb.results) {
|
|
765
|
-
arr = twikooDb.results
|
|
766
|
-
}
|
|
767
|
-
if (!arr) {
|
|
768
|
-
log('Valine 评论文件格式有误')
|
|
769
|
-
return
|
|
770
|
-
}
|
|
771
|
-
const comments = []
|
|
772
|
-
log(`共 ${arr.length} 条评论`)
|
|
773
|
-
for (const comment of arr) {
|
|
774
|
-
try {
|
|
775
|
-
const parsed = comment
|
|
776
|
-
if (comment._id.$oid) {
|
|
777
|
-
// 解决 id 历史数据问题
|
|
778
|
-
parsed._id = comment._id.$oid
|
|
779
|
-
}
|
|
780
|
-
comments.push(parsed)
|
|
781
|
-
log(`${comment.id} 解析成功`)
|
|
782
|
-
} catch (e) {
|
|
783
|
-
log(`${comment.id} 解析失败:${e.message}`)
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
log(`解析成功 ${comments.length} 条评论`)
|
|
787
|
-
const ids = await bulkSaveComments(comments)
|
|
788
|
-
log(`导入成功 ${ids.length} 条评论`)
|
|
789
|
-
return comments
|
|
790
|
-
}
|
|
791
|
-
|
|
792
477
|
// 批量导入评论
|
|
793
478
|
async function bulkSaveComments (comments) {
|
|
794
479
|
const batchRes = await db
|
|
@@ -878,239 +563,25 @@ async function save (data) {
|
|
|
878
563
|
return data
|
|
879
564
|
}
|
|
880
565
|
|
|
566
|
+
async function getParentComment (currentComment) {
|
|
567
|
+
const parentComment = await db
|
|
568
|
+
.collection('comment')
|
|
569
|
+
.where({ _id: currentComment.pid })
|
|
570
|
+
.get()
|
|
571
|
+
return parentComment.data[0]
|
|
572
|
+
}
|
|
573
|
+
|
|
881
574
|
// 异步垃圾检测、发送评论通知
|
|
882
575
|
async function postSubmit (comment, context) {
|
|
883
576
|
if (!isRecursion(context)) return { code: RES_CODE.FORBIDDEN }
|
|
884
577
|
// 垃圾检测
|
|
885
|
-
await postCheckSpam(comment)
|
|
578
|
+
const isSpam = await postCheckSpam(comment)
|
|
579
|
+
await saveSpamCheckResult(comment, isSpam)
|
|
886
580
|
// 发送通知
|
|
887
|
-
await sendNotice(comment)
|
|
581
|
+
await sendNotice(comment, config, getParentComment)
|
|
888
582
|
return { code: RES_CODE.SUCCESS }
|
|
889
583
|
}
|
|
890
584
|
|
|
891
|
-
// 发送通知
|
|
892
|
-
async function sendNotice (comment) {
|
|
893
|
-
if (comment.isSpam && config.NOTIFY_SPAM === 'false') return
|
|
894
|
-
await Promise.all([
|
|
895
|
-
noticeMaster(comment),
|
|
896
|
-
noticeReply(comment),
|
|
897
|
-
noticePushoo(comment)
|
|
898
|
-
]).catch(err => {
|
|
899
|
-
console.error('通知异常:', err)
|
|
900
|
-
})
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// 初始化邮件插件
|
|
904
|
-
async function initMailer ({ throwErr = false } = {}) {
|
|
905
|
-
try {
|
|
906
|
-
if (!config || !config.SMTP_USER || !config.SMTP_PASS) {
|
|
907
|
-
throw new Error('数据库配置不存在')
|
|
908
|
-
}
|
|
909
|
-
const transportConfig = {
|
|
910
|
-
auth: {
|
|
911
|
-
user: config.SMTP_USER,
|
|
912
|
-
pass: config.SMTP_PASS
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
if (config.SMTP_SERVICE) {
|
|
916
|
-
transportConfig.service = config.SMTP_SERVICE
|
|
917
|
-
} else if (config.SMTP_HOST) {
|
|
918
|
-
transportConfig.host = config.SMTP_HOST
|
|
919
|
-
transportConfig.port = parseInt(config.SMTP_PORT)
|
|
920
|
-
transportConfig.secure = config.SMTP_SECURE === 'true'
|
|
921
|
-
} else {
|
|
922
|
-
throw new Error('SMTP 服务器没有配置')
|
|
923
|
-
}
|
|
924
|
-
transporter = nodemailer.createTransport(transportConfig)
|
|
925
|
-
try {
|
|
926
|
-
const success = await transporter.verify()
|
|
927
|
-
if (success) console.log('SMTP 邮箱配置正常')
|
|
928
|
-
} catch (error) {
|
|
929
|
-
throw new Error('SMTP 邮箱配置异常:', error)
|
|
930
|
-
}
|
|
931
|
-
return true
|
|
932
|
-
} catch (e) {
|
|
933
|
-
console.error('邮件初始化异常:', e.message)
|
|
934
|
-
if (throwErr) throw e
|
|
935
|
-
return false
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
// 博主通知
|
|
940
|
-
async function noticeMaster (comment) {
|
|
941
|
-
if (!transporter) if (!await initMailer()) return
|
|
942
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
943
|
-
// 判断是否存在即时消息推送配置
|
|
944
|
-
const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
|
|
945
|
-
// 存在即时消息推送配置,则默认不发送邮件给博主
|
|
946
|
-
if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') return
|
|
947
|
-
const SITE_NAME = config.SITE_NAME
|
|
948
|
-
const NICK = comment.nick
|
|
949
|
-
const IMG = getAvatar(comment)
|
|
950
|
-
const IP = comment.ip
|
|
951
|
-
const MAIL = comment.mail
|
|
952
|
-
const COMMENT = comment.comment
|
|
953
|
-
const SITE_URL = config.SITE_URL
|
|
954
|
-
const POST_URL = appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
|
|
955
|
-
const emailSubject = config.MAIL_SUBJECT_ADMIN || `${SITE_NAME}上有新评论了`
|
|
956
|
-
let emailContent
|
|
957
|
-
if (config.MAIL_TEMPLATE_ADMIN) {
|
|
958
|
-
emailContent = config.MAIL_TEMPLATE_ADMIN
|
|
959
|
-
.replace(/\${SITE_URL}/g, SITE_URL)
|
|
960
|
-
.replace(/\${SITE_NAME}/g, SITE_NAME)
|
|
961
|
-
.replace(/\${NICK}/g, NICK)
|
|
962
|
-
.replace(/\${IMG}/g, IMG)
|
|
963
|
-
.replace(/\${IP}/g, IP)
|
|
964
|
-
.replace(/\${MAIL}/g, MAIL)
|
|
965
|
-
.replace(/\${COMMENT}/g, COMMENT)
|
|
966
|
-
.replace(/\${POST_URL}/g, POST_URL)
|
|
967
|
-
} else {
|
|
968
|
-
emailContent = `
|
|
969
|
-
<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;">
|
|
970
|
-
<h2 style="border-bottom:1px solid #dddddd;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;">
|
|
971
|
-
您在<a style="text-decoration:none;color: #12addb;" href="${SITE_URL}" target="_blank">${SITE_NAME}</a>上的文章有了新的评论
|
|
972
|
-
</h2>
|
|
973
|
-
<p><strong>${NICK}</strong>回复说:</p>
|
|
974
|
-
<div style="background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;">${COMMENT}</div>
|
|
975
|
-
<p>您可以点击<a style="text-decoration:none; color:#12addb" href="${POST_URL}" target="_blank">查看回复的完整內容</a><br></p>
|
|
976
|
-
</div>`
|
|
977
|
-
}
|
|
978
|
-
let sendResult
|
|
979
|
-
try {
|
|
980
|
-
sendResult = await transporter.sendMail({
|
|
981
|
-
from: `"${config.SENDER_NAME}" <${config.SENDER_EMAIL}>`,
|
|
982
|
-
to: config.BLOGGER_EMAIL || config.SENDER_EMAIL,
|
|
983
|
-
subject: emailSubject,
|
|
984
|
-
html: emailContent
|
|
985
|
-
})
|
|
986
|
-
} catch (e) {
|
|
987
|
-
sendResult = e
|
|
988
|
-
}
|
|
989
|
-
console.log('博主通知结果:', sendResult)
|
|
990
|
-
return sendResult
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// 即时消息通知
|
|
994
|
-
async function noticePushoo (comment) {
|
|
995
|
-
if (!config.PUSHOO_CHANNEL || !config.PUSHOO_TOKEN) {
|
|
996
|
-
console.log('没有配置 pushoo,放弃即时消息通知')
|
|
997
|
-
return
|
|
998
|
-
}
|
|
999
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1000
|
-
const pushContent = getIMPushContent(comment)
|
|
1001
|
-
const sendResult = await pushoo(config.PUSHOO_CHANNEL, {
|
|
1002
|
-
token: config.PUSHOO_TOKEN,
|
|
1003
|
-
title: pushContent.subject,
|
|
1004
|
-
content: pushContent.content,
|
|
1005
|
-
options: {
|
|
1006
|
-
bark: {
|
|
1007
|
-
url: pushContent.url
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
})
|
|
1011
|
-
console.log('即时消息通知结果:', sendResult)
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// 即时消息推送内容获取
|
|
1015
|
-
function getIMPushContent (comment) {
|
|
1016
|
-
const SITE_NAME = config.SITE_NAME
|
|
1017
|
-
const NICK = comment.nick
|
|
1018
|
-
const MAIL = comment.mail
|
|
1019
|
-
const IP = comment.ip
|
|
1020
|
-
const COMMENT = $(comment.comment).text()
|
|
1021
|
-
const SITE_URL = config.SITE_URL
|
|
1022
|
-
const POST_URL = appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
|
|
1023
|
-
const subject = config.MAIL_SUBJECT_ADMIN || `${SITE_NAME}有新评论了`
|
|
1024
|
-
const content = `评论人:${NICK} ([${MAIL}](mailto:${MAIL}))
|
|
1025
|
-
|
|
1026
|
-
评论人IP:${IP}
|
|
1027
|
-
|
|
1028
|
-
评论内容:${COMMENT}
|
|
1029
|
-
|
|
1030
|
-
原文链接:[${POST_URL}](${POST_URL})`
|
|
1031
|
-
return {
|
|
1032
|
-
subject,
|
|
1033
|
-
content,
|
|
1034
|
-
url: POST_URL
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// 回复通知
|
|
1039
|
-
async function noticeReply (currentComment) {
|
|
1040
|
-
if (!currentComment.pid) return
|
|
1041
|
-
if (!transporter) if (!await initMailer()) return
|
|
1042
|
-
let parentComment = await db
|
|
1043
|
-
.collection('comment')
|
|
1044
|
-
.where({ _id: currentComment.pid })
|
|
1045
|
-
.get()
|
|
1046
|
-
parentComment = parentComment.data[0]
|
|
1047
|
-
// 回复给博主,因为会发博主通知邮件,所以不再重复通知
|
|
1048
|
-
if (config.BLOGGER_EMAIL === parentComment.mail) return
|
|
1049
|
-
// 回复自己的评论,不邮件通知
|
|
1050
|
-
if (currentComment.mail === parentComment.mail) return
|
|
1051
|
-
const PARENT_NICK = parentComment.nick
|
|
1052
|
-
const IMG = getAvatar(currentComment)
|
|
1053
|
-
const PARENT_IMG = getAvatar(parentComment)
|
|
1054
|
-
const SITE_NAME = config.SITE_NAME
|
|
1055
|
-
const NICK = currentComment.nick
|
|
1056
|
-
const COMMENT = currentComment.comment
|
|
1057
|
-
const PARENT_COMMENT = parentComment.comment
|
|
1058
|
-
const POST_URL = appendHashToUrl(currentComment.href || config.SITE_URL + currentComment.url, currentComment.id)
|
|
1059
|
-
const SITE_URL = config.SITE_URL
|
|
1060
|
-
const emailSubject = config.MAIL_SUBJECT || `${PARENT_NICK},您在『${SITE_NAME}』上的评论收到了回复`
|
|
1061
|
-
let emailContent
|
|
1062
|
-
if (config.MAIL_TEMPLATE) {
|
|
1063
|
-
emailContent = config.MAIL_TEMPLATE
|
|
1064
|
-
.replace(/\${IMG}/g, IMG)
|
|
1065
|
-
.replace(/\${PARENT_IMG}/g, PARENT_IMG)
|
|
1066
|
-
.replace(/\${SITE_URL}/g, SITE_URL)
|
|
1067
|
-
.replace(/\${SITE_NAME}/g, SITE_NAME)
|
|
1068
|
-
.replace(/\${PARENT_NICK}/g, PARENT_NICK)
|
|
1069
|
-
.replace(/\${PARENT_COMMENT}/g, PARENT_COMMENT)
|
|
1070
|
-
.replace(/\${NICK}/g, NICK)
|
|
1071
|
-
.replace(/\${COMMENT}/g, COMMENT)
|
|
1072
|
-
.replace(/\${POST_URL}/g, POST_URL)
|
|
1073
|
-
} else {
|
|
1074
|
-
emailContent = `
|
|
1075
|
-
<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;">
|
|
1076
|
-
<h2 style="border-bottom:1px solid #dddddd;font-size:14px;font-weight:normal;padding:13px 0 10px 8px;">
|
|
1077
|
-
您在<a style="text-decoration:none;color: #12ADDB;" href="${SITE_URL}" target="_blank">${SITE_NAME}</a>上的评论有了新的回复
|
|
1078
|
-
</h2>
|
|
1079
|
-
${PARENT_NICK} 同学,您曾发表评论:
|
|
1080
|
-
<div style="padding:0 12px 0 12px;margin-top:18px">
|
|
1081
|
-
<div style="background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;">${PARENT_COMMENT}</div>
|
|
1082
|
-
<p><strong>${NICK}</strong>回复说:</p>
|
|
1083
|
-
<div style="background-color: #f5f5f5;padding: 10px 15px;margin:18px 0;word-wrap:break-word;">${COMMENT}</div>
|
|
1084
|
-
<p>
|
|
1085
|
-
您可以点击<a style="text-decoration:none; color:#12addb" href="${POST_URL}" target="_blank">查看回复的完整內容</a>,
|
|
1086
|
-
欢迎再次光临<a style="text-decoration:none; color:#12addb" href="${SITE_URL}" target="_blank">${SITE_NAME}</a>。<br>
|
|
1087
|
-
</p>
|
|
1088
|
-
</div>
|
|
1089
|
-
</div>`
|
|
1090
|
-
}
|
|
1091
|
-
let sendResult
|
|
1092
|
-
try {
|
|
1093
|
-
sendResult = await transporter.sendMail({
|
|
1094
|
-
from: `"${config.SENDER_NAME}" <${config.SENDER_EMAIL}>`,
|
|
1095
|
-
to: parentComment.mail,
|
|
1096
|
-
subject: emailSubject,
|
|
1097
|
-
html: emailContent
|
|
1098
|
-
})
|
|
1099
|
-
} catch (e) {
|
|
1100
|
-
sendResult = e
|
|
1101
|
-
}
|
|
1102
|
-
console.log('回复通知结果:', sendResult)
|
|
1103
|
-
return sendResult
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
function appendHashToUrl (url, hash) {
|
|
1107
|
-
if (url.indexOf('#') === -1) {
|
|
1108
|
-
return `${url}#${hash}`
|
|
1109
|
-
} else {
|
|
1110
|
-
return `${url.substring(0, url.indexOf('#'))}#${hash}`
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
585
|
// 将评论转为数据库存储格式
|
|
1115
586
|
async function parse (comment) {
|
|
1116
587
|
const timestamp = Date.now()
|
|
@@ -1131,7 +602,7 @@ async function parse (comment) {
|
|
|
1131
602
|
comment: DOMPurify.sanitize(comment.comment, { FORBID_TAGS: ['style'], FORBID_ATTR: ['style'] }),
|
|
1132
603
|
pid: comment.pid ? comment.pid : comment.rid,
|
|
1133
604
|
rid: comment.rid,
|
|
1134
|
-
isSpam: isAdminUser ? false : preCheckSpam(comment),
|
|
605
|
+
isSpam: isAdminUser ? false : preCheckSpam(comment, config),
|
|
1135
606
|
created: timestamp,
|
|
1136
607
|
updated: timestamp
|
|
1137
608
|
}
|
|
@@ -1178,86 +649,16 @@ async function limitFilter () {
|
|
|
1178
649
|
}
|
|
1179
650
|
}
|
|
1180
651
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
// 人工审核
|
|
1191
|
-
console.log('已使用人工审核模式,评论审核后才会发表~')
|
|
1192
|
-
return true
|
|
1193
|
-
} else if (config.FORBIDDEN_WORDS) {
|
|
1194
|
-
// 违禁词检测
|
|
1195
|
-
for (const forbiddenWord of config.FORBIDDEN_WORDS.split(',')) {
|
|
1196
|
-
if (comment.indexOf(forbiddenWord.trim()) !== -1 || nick.indexOf(forbiddenWord.trim()) !== -1) {
|
|
1197
|
-
console.log('包含违禁词,直接标记为垃圾评论~')
|
|
1198
|
-
return true
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
return false
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
// 后垃圾评论检测
|
|
1206
|
-
async function postCheckSpam (comment) {
|
|
1207
|
-
try {
|
|
1208
|
-
let isSpam
|
|
1209
|
-
if (comment.isSpam) {
|
|
1210
|
-
// 预检测没过的,就不再检测了
|
|
1211
|
-
isSpam = true
|
|
1212
|
-
} else if (config.QCLOUD_SECRET_ID && config.QCLOUD_SECRET_KEY) {
|
|
1213
|
-
// 腾讯云内容安全
|
|
1214
|
-
const client = new tencentcloud.tms.v20200713.Client({
|
|
1215
|
-
credential: { secretId: config.QCLOUD_SECRET_ID, secretKey: config.QCLOUD_SECRET_KEY },
|
|
1216
|
-
region: 'ap-shanghai',
|
|
1217
|
-
profile: { httpProfile: { endpoint: 'tms.tencentcloudapi.com' } }
|
|
1218
|
-
})
|
|
1219
|
-
const checkResult = await client.TextModeration({
|
|
1220
|
-
Content: CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(comment.comment)),
|
|
1221
|
-
Device: { IP: comment.ip },
|
|
1222
|
-
User: { Nickname: comment.nick }
|
|
1223
|
-
})
|
|
1224
|
-
console.log('腾讯云返回结果:', checkResult)
|
|
1225
|
-
isSpam = checkResult.EvilFlag !== 0
|
|
1226
|
-
} else if (config.AKISMET_KEY) {
|
|
1227
|
-
// Akismet
|
|
1228
|
-
const akismetClient = new AkismetClient({
|
|
1229
|
-
key: config.AKISMET_KEY,
|
|
1230
|
-
blog: config.SITE_URL
|
|
1231
|
-
})
|
|
1232
|
-
const isValid = await akismetClient.verifyKey()
|
|
1233
|
-
if (!isValid) {
|
|
1234
|
-
console.log('Akismet key 不可用:', config.AKISMET_KEY)
|
|
1235
|
-
return
|
|
1236
|
-
}
|
|
1237
|
-
isSpam = await akismetClient.checkSpam({
|
|
1238
|
-
user_ip: comment.ip,
|
|
1239
|
-
user_agent: comment.ua,
|
|
1240
|
-
permalink: comment.href,
|
|
1241
|
-
comment_type: comment.rid ? 'reply' : 'comment',
|
|
1242
|
-
comment_author: comment.nick,
|
|
1243
|
-
comment_author_email: comment.mail,
|
|
1244
|
-
comment_author_url: comment.link,
|
|
1245
|
-
comment_content: comment.comment
|
|
652
|
+
async function saveSpamCheckResult (comment, isSpam) {
|
|
653
|
+
comment.isSpam = isSpam
|
|
654
|
+
if (isSpam) {
|
|
655
|
+
await db
|
|
656
|
+
.collection('comment')
|
|
657
|
+
.doc(comment.id)
|
|
658
|
+
.update({
|
|
659
|
+
isSpam,
|
|
660
|
+
updated: Date.now()
|
|
1246
661
|
})
|
|
1247
|
-
}
|
|
1248
|
-
console.log('垃圾评论检测结果:', isSpam)
|
|
1249
|
-
comment.isSpam = isSpam
|
|
1250
|
-
if (isSpam) {
|
|
1251
|
-
await db
|
|
1252
|
-
.collection('comment')
|
|
1253
|
-
.doc(comment.id)
|
|
1254
|
-
.update({
|
|
1255
|
-
isSpam,
|
|
1256
|
-
updated: Date.now()
|
|
1257
|
-
})
|
|
1258
|
-
}
|
|
1259
|
-
} catch (err) {
|
|
1260
|
-
console.error('垃圾评论检测异常:', err)
|
|
1261
662
|
}
|
|
1262
663
|
}
|
|
1263
664
|
|
|
@@ -1353,14 +754,6 @@ async function getCommentsCount (event) {
|
|
|
1353
754
|
return res
|
|
1354
755
|
}
|
|
1355
756
|
|
|
1356
|
-
function getUrlsQuery (urls) {
|
|
1357
|
-
const query = []
|
|
1358
|
-
for (const url of urls) {
|
|
1359
|
-
if (url) query.push(...getUrlQuery(url))
|
|
1360
|
-
}
|
|
1361
|
-
return query
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
757
|
/**
|
|
1365
758
|
* 获取最新评论 API
|
|
1366
759
|
* @param {Boolean} event.includeReply 评论数是否包括回复,默认:false
|
|
@@ -1384,7 +777,7 @@ async function getRecentComments (event) {
|
|
|
1384
777
|
url: comment.url,
|
|
1385
778
|
href: comment.href,
|
|
1386
779
|
nick: comment.nick,
|
|
1387
|
-
avatar: getAvatar(comment),
|
|
780
|
+
avatar: getAvatar(comment, config),
|
|
1388
781
|
mailMd5: comment.mailMd5 || md5(comment.mail),
|
|
1389
782
|
link: comment.link,
|
|
1390
783
|
comment: comment.comment,
|
|
@@ -1399,183 +792,6 @@ async function getRecentComments (event) {
|
|
|
1399
792
|
return res
|
|
1400
793
|
}
|
|
1401
794
|
|
|
1402
|
-
async function emailTest (event) {
|
|
1403
|
-
const res = {}
|
|
1404
|
-
const isAdminUser = await isAdmin()
|
|
1405
|
-
if (isAdminUser) {
|
|
1406
|
-
try {
|
|
1407
|
-
// 邮件测试前清除 transporter,保证读取的是最新的配置
|
|
1408
|
-
transporter = null
|
|
1409
|
-
await initMailer({ throwErr: true })
|
|
1410
|
-
const sendResult = await transporter.sendMail({
|
|
1411
|
-
from: config.SENDER_EMAIL,
|
|
1412
|
-
to: event.mail || config.BLOGGER_EMAIL || config.SENDER_EMAIL,
|
|
1413
|
-
subject: 'Twikoo 邮件通知测试邮件',
|
|
1414
|
-
html: '如果您收到这封邮件,说明 Twikoo 邮件功能配置正确'
|
|
1415
|
-
})
|
|
1416
|
-
res.result = sendResult
|
|
1417
|
-
} catch (e) {
|
|
1418
|
-
res.message = e.message
|
|
1419
|
-
}
|
|
1420
|
-
} else {
|
|
1421
|
-
res.code = RES_CODE.NEED_LOGIN
|
|
1422
|
-
res.message = '请先登录'
|
|
1423
|
-
}
|
|
1424
|
-
return res
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
async function uploadImage (event) {
|
|
1428
|
-
const { photo, fileName } = event
|
|
1429
|
-
const res = {}
|
|
1430
|
-
try {
|
|
1431
|
-
if (!config.IMAGE_CDN || !config.IMAGE_CDN_TOKEN) {
|
|
1432
|
-
throw new Error('未配置图片上传服务')
|
|
1433
|
-
}
|
|
1434
|
-
// tip: qcloud 图床走前端上传,其他图床走后端上传
|
|
1435
|
-
if (config.IMAGE_CDN === '7bu') {
|
|
1436
|
-
await uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: 'https://7bu.top' })
|
|
1437
|
-
} else if (config.IMAGE_CDN === 'smms') {
|
|
1438
|
-
await uploadImageToSmms({ photo, fileName, config, res })
|
|
1439
|
-
} else if (isUrl(config.IMAGE_CDN)) {
|
|
1440
|
-
await uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN })
|
|
1441
|
-
}
|
|
1442
|
-
} catch (e) {
|
|
1443
|
-
console.error(e)
|
|
1444
|
-
res.code = RES_CODE.UPLOAD_FAILED
|
|
1445
|
-
res.err = e.message
|
|
1446
|
-
}
|
|
1447
|
-
return res
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
async function uploadImageToSmms ({ photo, fileName, config, res }) {
|
|
1451
|
-
// SM.MS 图床 https://sm.ms
|
|
1452
|
-
const formData = new FormData()
|
|
1453
|
-
formData.append('smfile', base64UrlToReadStream(photo, fileName))
|
|
1454
|
-
const uploadResult = await axios.post('https://sm.ms/api/v2/upload', formData, {
|
|
1455
|
-
headers: {
|
|
1456
|
-
...formData.getHeaders(),
|
|
1457
|
-
Authorization: config.IMAGE_CDN_TOKEN
|
|
1458
|
-
}
|
|
1459
|
-
})
|
|
1460
|
-
if (uploadResult.data.success) {
|
|
1461
|
-
res.data = uploadResult.data.data
|
|
1462
|
-
} else {
|
|
1463
|
-
throw new Error(uploadResult.data.message)
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1466
|
-
|
|
1467
|
-
async function uploadImageToLskyPro ({ photo, fileName, config, res, imageCdn }) {
|
|
1468
|
-
// 自定义兰空图床(v2)URL
|
|
1469
|
-
const formData = new FormData()
|
|
1470
|
-
formData.append('file', base64UrlToReadStream(photo, fileName))
|
|
1471
|
-
const url = `${imageCdn}/api/v1/upload`
|
|
1472
|
-
let token = config.IMAGE_CDN_TOKEN
|
|
1473
|
-
if (!token.startsWith('Bearer')) {
|
|
1474
|
-
token = `Bearer ${token}`
|
|
1475
|
-
}
|
|
1476
|
-
const uploadResult = await axios.post(url, formData, {
|
|
1477
|
-
headers: {
|
|
1478
|
-
...formData.getHeaders(),
|
|
1479
|
-
Authorization: token
|
|
1480
|
-
}
|
|
1481
|
-
})
|
|
1482
|
-
if (uploadResult.data.status) {
|
|
1483
|
-
res.data = uploadResult.data.data
|
|
1484
|
-
res.data.url = res.data.links.url
|
|
1485
|
-
} else {
|
|
1486
|
-
throw new Error(uploadResult.data.message)
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
function base64UrlToReadStream (base64Url, fileName) {
|
|
1491
|
-
const base64 = base64Url.split(';base64,').pop()
|
|
1492
|
-
const path = `/tmp/${fileName}`
|
|
1493
|
-
fs.writeFileSync(path, base64, { encoding: 'base64' })
|
|
1494
|
-
return fs.createReadStream(path)
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
function isUrl (s) {
|
|
1498
|
-
return /^http(s)?:\/\//.test(s)
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
function getAvatar (comment) {
|
|
1502
|
-
if (comment.avatar) {
|
|
1503
|
-
return comment.avatar
|
|
1504
|
-
} else {
|
|
1505
|
-
const gravatarCdn = config.GRAVATAR_CDN || 'cravatar.cn'
|
|
1506
|
-
const defaultGravatar = config.DEFAULT_GRAVATAR || 'identicon'
|
|
1507
|
-
const mailMd5 = comment.mailMd5 || md5(comment.mail)
|
|
1508
|
-
return `https://${gravatarCdn}/avatar/${mailMd5}?d=${defaultGravatar}`
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
function isQQ (mail) {
|
|
1513
|
-
return /^[1-9][0-9]{4,10}$/.test(mail) ||
|
|
1514
|
-
/^[1-9][0-9]{4,10}@qq.com$/i.test(mail)
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function addQQMailSuffix (mail) {
|
|
1518
|
-
if (/^[1-9][0-9]{4,10}$/.test(mail)) return `${mail}@qq.com`
|
|
1519
|
-
else return mail
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
async function getQQAvatar (qq) {
|
|
1523
|
-
try {
|
|
1524
|
-
const qqNum = qq.replace(/@qq.com/ig, '')
|
|
1525
|
-
const result = await axios.get(`https://ptlogin2.qq.com/getface?imgtype=4&uin=${qqNum}`)
|
|
1526
|
-
if (result && result.data) {
|
|
1527
|
-
const start = result.data.indexOf('http')
|
|
1528
|
-
const end = result.data.indexOf('"', start)
|
|
1529
|
-
if (start === -1 || end === -1) return null
|
|
1530
|
-
return result.data.substring(start, end)
|
|
1531
|
-
}
|
|
1532
|
-
} catch (e) {
|
|
1533
|
-
console.error('获取 QQ 头像失败:', e)
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
function getConfig () {
|
|
1538
|
-
return {
|
|
1539
|
-
code: RES_CODE.SUCCESS,
|
|
1540
|
-
config: {
|
|
1541
|
-
VERSION,
|
|
1542
|
-
SITE_NAME: config.SITE_NAME,
|
|
1543
|
-
SITE_URL: config.SITE_URL,
|
|
1544
|
-
MASTER_TAG: config.MASTER_TAG,
|
|
1545
|
-
COMMENT_BG_IMG: config.COMMENT_BG_IMG,
|
|
1546
|
-
GRAVATAR_CDN: config.GRAVATAR_CDN,
|
|
1547
|
-
DEFAULT_GRAVATAR: config.DEFAULT_GRAVATAR,
|
|
1548
|
-
SHOW_IMAGE: config.SHOW_IMAGE || 'true',
|
|
1549
|
-
IMAGE_CDN: config.IMAGE_CDN,
|
|
1550
|
-
IMAGE_CDN_TOKEN: config.IMAGE_CDN_TOKEN,
|
|
1551
|
-
SHOW_EMOTION: config.SHOW_EMOTION || 'true',
|
|
1552
|
-
EMOTION_CDN: config.EMOTION_CDN,
|
|
1553
|
-
COMMENT_PLACEHOLDER: config.COMMENT_PLACEHOLDER,
|
|
1554
|
-
REQUIRED_FIELDS: config.REQUIRED_FIELDS,
|
|
1555
|
-
HIDE_ADMIN_CRYPT: config.HIDE_ADMIN_CRYPT,
|
|
1556
|
-
HIGHLIGHT: config.HIGHLIGHT || 'true',
|
|
1557
|
-
HIGHLIGHT_THEME: config.HIGHLIGHT_THEME,
|
|
1558
|
-
LIMIT_LENGTH: config.LIMIT_LENGTH
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
}
|
|
1562
|
-
|
|
1563
|
-
async function getConfigForAdmin () {
|
|
1564
|
-
const isAdminUser = await isAdmin()
|
|
1565
|
-
if (isAdminUser) {
|
|
1566
|
-
delete config.CREDENTIALS
|
|
1567
|
-
return {
|
|
1568
|
-
code: RES_CODE.SUCCESS,
|
|
1569
|
-
config
|
|
1570
|
-
}
|
|
1571
|
-
} else {
|
|
1572
|
-
return {
|
|
1573
|
-
code: RES_CODE.NEED_LOGIN,
|
|
1574
|
-
message: '请先登录'
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
795
|
// 修改配置
|
|
1580
796
|
async function setConfig (event) {
|
|
1581
797
|
const isAdminUser = await isAdmin()
|
|
@@ -1660,29 +876,6 @@ async function isAdmin () {
|
|
|
1660
876
|
return ADMIN_USER_ID === userInfo.userInfo.customUserId
|
|
1661
877
|
}
|
|
1662
878
|
|
|
1663
|
-
/**
|
|
1664
|
-
* 获取 IP 属地
|
|
1665
|
-
* @param detail true 返回省市运营商,false 只返回省
|
|
1666
|
-
* @returns {String}
|
|
1667
|
-
*/
|
|
1668
|
-
function getIpRegion ({ ip, detail = false }) {
|
|
1669
|
-
if (!ip) return ''
|
|
1670
|
-
try {
|
|
1671
|
-
const { region } = ipRegionSearcher.binarySearchSync(ip)
|
|
1672
|
-
const [country,, province, city, isp] = region.split('|')
|
|
1673
|
-
// 有省显示省,没有省显示国家
|
|
1674
|
-
const area = province.trim() ? province : country
|
|
1675
|
-
if (detail) {
|
|
1676
|
-
return area === city ? [city, isp].join(' ') : [area, city, isp].join(' ')
|
|
1677
|
-
} else {
|
|
1678
|
-
return area
|
|
1679
|
-
}
|
|
1680
|
-
} catch (e) {
|
|
1681
|
-
console.error('IP 属地查询失败:', e)
|
|
1682
|
-
return ''
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
879
|
// 判断是否为递归调用(即云函数调用自身)
|
|
1687
880
|
function isRecursion (context) {
|
|
1688
881
|
const envObj = tcb.getCloudbaseContext(context)
|
|
@@ -1702,12 +895,3 @@ async function createCollections () {
|
|
|
1702
895
|
}
|
|
1703
896
|
return res
|
|
1704
897
|
}
|
|
1705
|
-
|
|
1706
|
-
// 请求参数校验
|
|
1707
|
-
function validate (event = {}, requiredParams = []) {
|
|
1708
|
-
for (const requiredParam of requiredParams) {
|
|
1709
|
-
if (!event[requiredParam]) {
|
|
1710
|
-
throw new Error(`参数"${requiredParam}"不合法`)
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
}
|