twikoo-vercel 1.4.16 → 1.5.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/api/index.js +66 -124
- package/package.json +3 -2
package/api/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Twikoo vercel function v1.
|
|
2
|
+
* Twikoo vercel function v1.5.0
|
|
3
3
|
* (c) 2020-present iMaeGoo
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -11,7 +11,6 @@ const md5 = require('blueimp-md5') // MD5 加解密
|
|
|
11
11
|
const bowser = require('bowser') // UserAgent 格式化
|
|
12
12
|
const nodemailer = require('nodemailer') // 发送邮件
|
|
13
13
|
const axios = require('axios') // 发送 REST 请求
|
|
14
|
-
const qs = require('querystring') // URL 参数格式化
|
|
15
14
|
const $ = require('cheerio') // jQuery 服务器版
|
|
16
15
|
const { AkismetClient } = require('akismet-api') // 反垃圾 API
|
|
17
16
|
const createDOMPurify = require('dompurify') // 反 XSS
|
|
@@ -21,13 +20,16 @@ const marked = require('marked') // Markdown 解析
|
|
|
21
20
|
const CryptoJS = require('crypto-js') // 编解码
|
|
22
21
|
const tencentcloud = require('tencentcloud-sdk-nodejs') // 腾讯云 API NODEJS SDK
|
|
23
22
|
const { v4: uuidv4 } = require('uuid') // 用户 id 生成
|
|
23
|
+
const fs = require('fs')
|
|
24
|
+
const FormData = require('form-data') // 图片上传
|
|
25
|
+
const pushoo = require('pushoo').default
|
|
24
26
|
|
|
25
27
|
// 初始化反 XSS
|
|
26
28
|
const window = new JSDOM('').window
|
|
27
29
|
const DOMPurify = createDOMPurify(window)
|
|
28
30
|
|
|
29
31
|
// 常量 / constants
|
|
30
|
-
const VERSION = '1.
|
|
32
|
+
const VERSION = '1.5.0'
|
|
31
33
|
const RES_CODE = {
|
|
32
34
|
SUCCESS: 0,
|
|
33
35
|
NO_PARAM: 100,
|
|
@@ -41,7 +43,8 @@ const RES_CODE = {
|
|
|
41
43
|
PASS_NOT_MATCH: 1023,
|
|
42
44
|
NEED_LOGIN: 1024,
|
|
43
45
|
FORBIDDEN: 1403,
|
|
44
|
-
AKISMET_ERROR: 1030
|
|
46
|
+
AKISMET_ERROR: 1030,
|
|
47
|
+
UPLOAD_FAILED: 1040
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
// 全局变量 / variables
|
|
@@ -126,6 +129,9 @@ module.exports = async (requestArg, responseArg) => {
|
|
|
126
129
|
case 'EMAIL_TEST': // >= 1.4.6
|
|
127
130
|
res = await emailTest(event)
|
|
128
131
|
break
|
|
132
|
+
case 'UPLOAD_IMAGE': // >= 1.5.0
|
|
133
|
+
res = await uploadImage(event)
|
|
134
|
+
break
|
|
129
135
|
default:
|
|
130
136
|
if (event.event) {
|
|
131
137
|
res.code = RES_CODE.EVENT_NOT_EXIST
|
|
@@ -888,12 +894,7 @@ async function sendNotice (comment) {
|
|
|
888
894
|
await Promise.all([
|
|
889
895
|
noticeMaster(comment),
|
|
890
896
|
noticeReply(comment),
|
|
891
|
-
|
|
892
|
-
noticePushPlus(comment),
|
|
893
|
-
noticeWeComPush(comment),
|
|
894
|
-
noticeDingTalkHook(comment),
|
|
895
|
-
noticePushdeer(comment),
|
|
896
|
-
noticeQQ(comment)
|
|
897
|
+
noticePushoo(comment)
|
|
897
898
|
]).catch(console.error)
|
|
898
899
|
return { code: RES_CODE.SUCCESS }
|
|
899
900
|
}
|
|
@@ -938,16 +939,8 @@ async function initMailer ({ throwErr = false } = {}) {
|
|
|
938
939
|
async function noticeMaster (comment) {
|
|
939
940
|
if (!transporter) if (!await initMailer()) return
|
|
940
941
|
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
941
|
-
const IM_PUSH_CONFIGS = [
|
|
942
|
-
'SC_SENDKEY',
|
|
943
|
-
'QM_SENDKEY',
|
|
944
|
-
'PUSH_PLUS_TOKEN',
|
|
945
|
-
'WECOM_API_URL',
|
|
946
|
-
'DINGTALK_WEBHOOK_URL',
|
|
947
|
-
'PUSHDEER_KEY'
|
|
948
|
-
]
|
|
949
942
|
// 判断是否存在即时消息推送配置
|
|
950
|
-
const hasIMPushConfig =
|
|
943
|
+
const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
|
|
951
944
|
// 存在即时消息推送配置,则默认不发送邮件给博主
|
|
952
945
|
if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') return
|
|
953
946
|
const SITE_NAME = config.SITE_NAME
|
|
@@ -996,110 +989,24 @@ async function noticeMaster (comment) {
|
|
|
996
989
|
return sendResult
|
|
997
990
|
}
|
|
998
991
|
|
|
999
|
-
//
|
|
1000
|
-
async function
|
|
1001
|
-
if (!config.
|
|
1002
|
-
console.log('没有配置
|
|
992
|
+
// 即时消息通知
|
|
993
|
+
async function noticePushoo (comment) {
|
|
994
|
+
if (!config.PUSHOO_CHANNEL || !config.PUSHOO_TOKEN) {
|
|
995
|
+
console.log('没有配置 pushoo,放弃即时消息通知')
|
|
1003
996
|
return
|
|
1004
997
|
}
|
|
1005
998
|
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1006
999
|
const pushContent = getIMPushContent(comment)
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
text: pushContent.subject,
|
|
1010
|
-
desp: pushContent.content
|
|
1011
|
-
}
|
|
1012
|
-
if (config.SC_SENDKEY.substring(0, 3).toLowerCase() === 'sct') {
|
|
1013
|
-
// 兼容 server 酱测试专版
|
|
1014
|
-
scApiUrl = 'https://sctapi.ftqq.com'
|
|
1015
|
-
scApiParam = {
|
|
1016
|
-
title: pushContent.subject,
|
|
1017
|
-
desp: pushContent.content
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
const sendResult = await axios.post(`${scApiUrl}/${config.SC_SENDKEY}.send`, qs.stringify(scApiParam), {
|
|
1021
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
1022
|
-
})
|
|
1023
|
-
console.log('微信通知结果:', sendResult)
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// pushplus 通知
|
|
1027
|
-
async function noticePushPlus (comment) {
|
|
1028
|
-
if (!config.PUSH_PLUS_TOKEN) {
|
|
1029
|
-
console.log('没有配置 pushplus,放弃通知')
|
|
1030
|
-
return
|
|
1031
|
-
}
|
|
1032
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1033
|
-
const pushContent = getIMPushContent(comment)
|
|
1034
|
-
const ppApiUrl = 'http://pushplus.hxtrip.com/send'
|
|
1035
|
-
const ppApiParam = {
|
|
1036
|
-
token: config.PUSH_PLUS_TOKEN,
|
|
1000
|
+
const sendResult = await pushoo(config.PUSHOO_CHANNEL, {
|
|
1001
|
+
token: config.PUSHOO_TOKEN,
|
|
1037
1002
|
title: pushContent.subject,
|
|
1038
1003
|
content: pushContent.content
|
|
1039
|
-
}
|
|
1040
|
-
const sendResult = await axios.post(ppApiUrl, ppApiParam)
|
|
1041
|
-
console.log('pushplus 通知结果:', sendResult)
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// 自定义WeCom企业微信api通知
|
|
1045
|
-
async function noticeWeComPush (comment) {
|
|
1046
|
-
if (!config.WECOM_API_URL) {
|
|
1047
|
-
console.log('未配置 WECOM_API_URL,跳过企业微信推送')
|
|
1048
|
-
return
|
|
1049
|
-
}
|
|
1050
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1051
|
-
const SITE_URL = config.SITE_URL
|
|
1052
|
-
const WeComContent = config.SITE_NAME + '有新评论啦!🎉🎉' + '\n\n' + '@' + comment.nick + '说:' + $(comment.comment).text() + '\n' + 'E-mail: ' + comment.mail + '\n' + 'IP: ' + comment.ip + '\n' + '点此查看完整内容:' + appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
|
|
1053
|
-
const WeComApiContent = encodeURIComponent(WeComContent)
|
|
1054
|
-
const WeComApiUrl = config.WECOM_API_URL
|
|
1055
|
-
const sendResult = await axios.get(WeComApiUrl + WeComApiContent)
|
|
1056
|
-
console.log('WinxinPush 通知结果:', sendResult)
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
// 自定义钉钉WebHook通知
|
|
1060
|
-
async function noticeDingTalkHook (comment) {
|
|
1061
|
-
if (!config.DINGTALK_WEBHOOK_URL) {
|
|
1062
|
-
console.log('没有配置 DingTalk_WebHook,放弃钉钉WebHook推送')
|
|
1063
|
-
return
|
|
1064
|
-
}
|
|
1065
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1066
|
-
const DingTalkContent = config.SITE_NAME + '有新评论啦!🎉🎉' + '\n\n' + '@' + comment.nick + ' 说:' + $(comment.comment).text() + '\n' + 'E-mail: ' + comment.mail + '\n' + 'IP: ' + comment.ip + '\n' + '点此查看完整内容:' + appendHashToUrl(comment.href || config.SITE_URL + comment.url, comment.id)
|
|
1067
|
-
const sendResult = await axios.post(config.DINGTALK_WEBHOOK_URL, { msgtype: 'text', text: { content: DingTalkContent } })
|
|
1068
|
-
console.log('钉钉WebHook 通知结果:', sendResult)
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
// QQ通知
|
|
1072
|
-
async function noticeQQ (comment) {
|
|
1073
|
-
if (!config.QM_SENDKEY) {
|
|
1074
|
-
console.log('没有配置 qmsg 酱,放弃QQ通知')
|
|
1075
|
-
return
|
|
1076
|
-
}
|
|
1077
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1078
|
-
const pushContent = getIMPushContent(comment, { withUrl: false })
|
|
1079
|
-
const qmApiUrl = 'https://qmsg.zendee.cn'
|
|
1080
|
-
const qmApiParam = {
|
|
1081
|
-
msg: pushContent.subject + '\n' + pushContent.content.replace(/<br>/g, '\n')
|
|
1082
|
-
}
|
|
1083
|
-
const sendResult = await axios.post(`${qmApiUrl}/send/${config.QM_SENDKEY}`, qs.stringify(qmApiParam), {
|
|
1084
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
1085
1004
|
})
|
|
1086
|
-
console.log('
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
async function noticePushdeer (comment) {
|
|
1090
|
-
if (!config.PUSHDEER_KEY) return
|
|
1091
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1092
|
-
const pushContent = getIMPushContent(comment, { markdown: true })
|
|
1093
|
-
const sendResult = await axios.post('https://api2.pushdeer.com/message/push', {
|
|
1094
|
-
pushkey: config.PUSHDEER_KEY,
|
|
1095
|
-
text: pushContent.subject,
|
|
1096
|
-
desp: pushContent.content
|
|
1097
|
-
})
|
|
1098
|
-
console.log('Pushdeer 通知结果:', sendResult)
|
|
1005
|
+
console.log('即时消息通知结果:', sendResult)
|
|
1099
1006
|
}
|
|
1100
1007
|
|
|
1101
1008
|
// 即时消息推送内容获取
|
|
1102
|
-
function getIMPushContent (comment
|
|
1009
|
+
function getIMPushContent (comment) {
|
|
1103
1010
|
const SITE_NAME = config.SITE_NAME
|
|
1104
1011
|
const NICK = comment.nick
|
|
1105
1012
|
const MAIL = comment.mail
|
|
@@ -1108,14 +1015,13 @@ function getIMPushContent (comment, { withUrl = true, markdown = false }) {
|
|
|
1108
1015
|
const SITE_URL = config.SITE_URL
|
|
1109
1016
|
const POST_URL = appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
|
|
1110
1017
|
const subject = config.MAIL_SUBJECT_ADMIN || `${SITE_NAME}有新评论了`
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
}
|
|
1018
|
+
const content = `评论人:${NICK} ([${MAIL}](mailto:${MAIL}))
|
|
1019
|
+
|
|
1020
|
+
评论人IP:${IP}
|
|
1021
|
+
|
|
1022
|
+
评论内容:${COMMENT}
|
|
1023
|
+
|
|
1024
|
+
原文链接:[${POST_URL}](${POST_URL})`
|
|
1119
1025
|
return {
|
|
1120
1026
|
subject,
|
|
1121
1027
|
content
|
|
@@ -1232,7 +1138,8 @@ async function parse (comment) {
|
|
|
1232
1138
|
// 限流
|
|
1233
1139
|
async function limitFilter () {
|
|
1234
1140
|
// 限制每个 IP 每 10 分钟发表的评论数量
|
|
1235
|
-
|
|
1141
|
+
let limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
|
|
1142
|
+
if (Number.isNaN(limitPerMinute)) limitPerMinute = 10
|
|
1236
1143
|
if (limitPerMinute) {
|
|
1237
1144
|
const count = await db
|
|
1238
1145
|
.collection('comment')
|
|
@@ -1245,7 +1152,8 @@ async function limitFilter () {
|
|
|
1245
1152
|
}
|
|
1246
1153
|
}
|
|
1247
1154
|
// 限制所有 IP 每 10 分钟发表的评论数量
|
|
1248
|
-
|
|
1155
|
+
let limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
|
|
1156
|
+
if (Number.isNaN(limitPerMinuteAll)) limitPerMinuteAll = 10
|
|
1249
1157
|
if (limitPerMinuteAll) {
|
|
1250
1158
|
const count = await db
|
|
1251
1159
|
.collection('comment')
|
|
@@ -1499,6 +1407,41 @@ async function emailTest (event) {
|
|
|
1499
1407
|
return res
|
|
1500
1408
|
}
|
|
1501
1409
|
|
|
1410
|
+
async function uploadImage (event) {
|
|
1411
|
+
const { photo, fileName } = event
|
|
1412
|
+
const res = {}
|
|
1413
|
+
try {
|
|
1414
|
+
if (!config.IMAGE_CDN_TOKEN) {
|
|
1415
|
+
throw new Error('未配置图片上传服务')
|
|
1416
|
+
}
|
|
1417
|
+
const formData = new FormData()
|
|
1418
|
+
formData.append('image', base64UrlToReadStream(photo, fileName))
|
|
1419
|
+
const uploadResult = await axios.post('https://7bu.top/api/upload', formData, {
|
|
1420
|
+
headers: {
|
|
1421
|
+
...formData.getHeaders(),
|
|
1422
|
+
token: config.IMAGE_CDN_TOKEN
|
|
1423
|
+
}
|
|
1424
|
+
})
|
|
1425
|
+
if (uploadResult.data.code === 200) {
|
|
1426
|
+
res.data = uploadResult.data.data
|
|
1427
|
+
} else {
|
|
1428
|
+
throw new Error(uploadResult.data.msg)
|
|
1429
|
+
}
|
|
1430
|
+
} catch (e) {
|
|
1431
|
+
console.error(e)
|
|
1432
|
+
res.code = RES_CODE.UPLOAD_FAILED
|
|
1433
|
+
res.err = e.message
|
|
1434
|
+
}
|
|
1435
|
+
return res
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function base64UrlToReadStream (base64Url, fileName) {
|
|
1439
|
+
const base64 = base64Url.split(';base64,').pop()
|
|
1440
|
+
const path = `/tmp/${fileName}`
|
|
1441
|
+
fs.writeFileSync(path, base64, { encoding: 'base64' })
|
|
1442
|
+
return fs.createReadStream(path)
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1502
1445
|
function getAvatar (comment) {
|
|
1503
1446
|
if (comment.avatar) {
|
|
1504
1447
|
return comment.avatar
|
|
@@ -1549,7 +1492,6 @@ async function getConfig () {
|
|
|
1549
1492
|
DEFAULT_GRAVATAR: config.DEFAULT_GRAVATAR,
|
|
1550
1493
|
SHOW_IMAGE: config.SHOW_IMAGE || 'true',
|
|
1551
1494
|
IMAGE_CDN: config.IMAGE_CDN,
|
|
1552
|
-
IMAGE_CDN_TOKEN: config.IMAGE_CDN_TOKEN,
|
|
1553
1495
|
SHOW_EMOTION: config.SHOW_EMOTION || 'true',
|
|
1554
1496
|
EMOTION_CDN: config.EMOTION_CDN,
|
|
1555
1497
|
COMMENT_PLACEHOLDER: config.COMMENT_PLACEHOLDER,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twikoo-vercel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "A simple comment system based on Tencent CloudBase (tcb).",
|
|
5
5
|
"author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,11 +18,12 @@
|
|
|
18
18
|
"cheerio": "1.0.0-rc.5",
|
|
19
19
|
"crypto-js": "^4.0.0",
|
|
20
20
|
"dompurify": "^2.2.6",
|
|
21
|
+
"form-data": "^4.0.0",
|
|
21
22
|
"jsdom": "^16.4.0",
|
|
22
23
|
"marked": "^4.0.12",
|
|
23
24
|
"mongodb": "^3.6.3",
|
|
24
25
|
"nodemailer": "^6.4.17",
|
|
25
|
-
"
|
|
26
|
+
"pushoo": "latest",
|
|
26
27
|
"tencentcloud-sdk-nodejs": "^4.0.65",
|
|
27
28
|
"uuid": "^8.3.2",
|
|
28
29
|
"xml2js": "^0.4.23"
|