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