twikoo-vercel 1.4.18 → 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 -143
- 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,13 +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
|
-
noticeQQAPI(comment)
|
|
897
|
+
noticePushoo(comment)
|
|
898
898
|
]).catch(console.error)
|
|
899
899
|
return { code: RES_CODE.SUCCESS }
|
|
900
900
|
}
|
|
@@ -939,16 +939,8 @@ async function initMailer ({ throwErr = false } = {}) {
|
|
|
939
939
|
async function noticeMaster (comment) {
|
|
940
940
|
if (!transporter) if (!await initMailer()) return
|
|
941
941
|
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
942
|
-
const IM_PUSH_CONFIGS = [
|
|
943
|
-
'SC_SENDKEY',
|
|
944
|
-
'QM_SENDKEY',
|
|
945
|
-
'PUSH_PLUS_TOKEN',
|
|
946
|
-
'WECOM_API_URL',
|
|
947
|
-
'DINGTALK_WEBHOOK_URL',
|
|
948
|
-
'PUSHDEER_KEY'
|
|
949
|
-
]
|
|
950
942
|
// 判断是否存在即时消息推送配置
|
|
951
|
-
const hasIMPushConfig =
|
|
943
|
+
const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
|
|
952
944
|
// 存在即时消息推送配置,则默认不发送邮件给博主
|
|
953
945
|
if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') return
|
|
954
946
|
const SITE_NAME = config.SITE_NAME
|
|
@@ -997,125 +989,24 @@ async function noticeMaster (comment) {
|
|
|
997
989
|
return sendResult
|
|
998
990
|
}
|
|
999
991
|
|
|
1000
|
-
//
|
|
1001
|
-
async function
|
|
1002
|
-
if (!config.
|
|
1003
|
-
console.log('没有配置
|
|
992
|
+
// 即时消息通知
|
|
993
|
+
async function noticePushoo (comment) {
|
|
994
|
+
if (!config.PUSHOO_CHANNEL || !config.PUSHOO_TOKEN) {
|
|
995
|
+
console.log('没有配置 pushoo,放弃即时消息通知')
|
|
1004
996
|
return
|
|
1005
997
|
}
|
|
1006
998
|
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1007
999
|
const pushContent = getIMPushContent(comment)
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
text: pushContent.subject,
|
|
1011
|
-
desp: pushContent.content
|
|
1012
|
-
}
|
|
1013
|
-
if (config.SC_SENDKEY.substring(0, 3).toLowerCase() === 'sct') {
|
|
1014
|
-
// 兼容 server 酱测试专版
|
|
1015
|
-
scApiUrl = 'https://sctapi.ftqq.com'
|
|
1016
|
-
scApiParam = {
|
|
1017
|
-
title: pushContent.subject,
|
|
1018
|
-
desp: pushContent.content
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
const sendResult = await axios.post(`${scApiUrl}/${config.SC_SENDKEY}.send`, qs.stringify(scApiParam), {
|
|
1022
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
1023
|
-
})
|
|
1024
|
-
console.log('微信通知结果:', sendResult)
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
// pushplus 通知
|
|
1028
|
-
async function noticePushPlus (comment) {
|
|
1029
|
-
if (!config.PUSH_PLUS_TOKEN) {
|
|
1030
|
-
console.log('没有配置 pushplus,放弃通知')
|
|
1031
|
-
return
|
|
1032
|
-
}
|
|
1033
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1034
|
-
const pushContent = getIMPushContent(comment)
|
|
1035
|
-
const ppApiUrl = 'http://pushplus.hxtrip.com/send'
|
|
1036
|
-
const ppApiParam = {
|
|
1037
|
-
token: config.PUSH_PLUS_TOKEN,
|
|
1000
|
+
const sendResult = await pushoo(config.PUSHOO_CHANNEL, {
|
|
1001
|
+
token: config.PUSHOO_TOKEN,
|
|
1038
1002
|
title: pushContent.subject,
|
|
1039
1003
|
content: pushContent.content
|
|
1040
|
-
}
|
|
1041
|
-
const sendResult = await axios.post(ppApiUrl, ppApiParam)
|
|
1042
|
-
console.log('pushplus 通知结果:', sendResult)
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
// 自定义WeCom企业微信api通知
|
|
1046
|
-
async function noticeWeComPush (comment) {
|
|
1047
|
-
if (!config.WECOM_API_URL) {
|
|
1048
|
-
console.log('未配置 WECOM_API_URL,跳过企业微信推送')
|
|
1049
|
-
return
|
|
1050
|
-
}
|
|
1051
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1052
|
-
const SITE_URL = config.SITE_URL
|
|
1053
|
-
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)
|
|
1054
|
-
const WeComApiContent = encodeURIComponent(WeComContent)
|
|
1055
|
-
const WeComApiUrl = config.WECOM_API_URL
|
|
1056
|
-
const sendResult = await axios.get(WeComApiUrl + WeComApiContent)
|
|
1057
|
-
console.log('WinxinPush 通知结果:', sendResult)
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
// 自定义钉钉WebHook通知
|
|
1061
|
-
async function noticeDingTalkHook (comment) {
|
|
1062
|
-
if (!config.DINGTALK_WEBHOOK_URL) {
|
|
1063
|
-
console.log('没有配置 DingTalk_WebHook,放弃钉钉WebHook推送')
|
|
1064
|
-
return
|
|
1065
|
-
}
|
|
1066
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1067
|
-
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)
|
|
1068
|
-
const sendResult = await axios.post(config.DINGTALK_WEBHOOK_URL, { msgtype: 'text', text: { content: DingTalkContent } })
|
|
1069
|
-
console.log('钉钉WebHook 通知结果:', sendResult)
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// QQ通知
|
|
1073
|
-
async function noticeQQ (comment) {
|
|
1074
|
-
if (!config.QM_SENDKEY) {
|
|
1075
|
-
console.log('没有配置 qmsg 酱,放弃QQ通知')
|
|
1076
|
-
return
|
|
1077
|
-
}
|
|
1078
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1079
|
-
const pushContent = getIMPushContent(comment, { withUrl: false })
|
|
1080
|
-
const qmApiUrl = 'https://qmsg.zendee.cn'
|
|
1081
|
-
const qmApiParam = {
|
|
1082
|
-
msg: pushContent.subject + '\n' + pushContent.content.replace(/<br>/g, '\n')
|
|
1083
|
-
}
|
|
1084
|
-
const sendResult = await axios.post(`${qmApiUrl}/send/${config.QM_SENDKEY}`, qs.stringify(qmApiParam), {
|
|
1085
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
1086
1004
|
})
|
|
1087
|
-
console.log('
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
async function noticePushdeer (comment) {
|
|
1091
|
-
if (!config.PUSHDEER_KEY) return
|
|
1092
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1093
|
-
const pushContent = getIMPushContent(comment, { markdown: true })
|
|
1094
|
-
const sendResult = await axios.post('https://api2.pushdeer.com/message/push', {
|
|
1095
|
-
pushkey: config.PUSHDEER_KEY,
|
|
1096
|
-
text: pushContent.subject,
|
|
1097
|
-
desp: pushContent.content
|
|
1098
|
-
})
|
|
1099
|
-
console.log('Pushdeer 通知结果:', sendResult)
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// QQ私有化API通知
|
|
1103
|
-
async function noticeQQAPI (comment) {
|
|
1104
|
-
if (!config.QQ_API) {
|
|
1105
|
-
console.log('没有配置QQ私有化api,放弃QQ通知')
|
|
1106
|
-
return
|
|
1107
|
-
}
|
|
1108
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1109
|
-
const pushContent = getIMPushContent(comment)
|
|
1110
|
-
const qqApiParam = {
|
|
1111
|
-
message: pushContent.subject + '\n' + pushContent.content.replace(/<br>/g, '\n')
|
|
1112
|
-
}
|
|
1113
|
-
const sendResult = await axios.post(`${config.QQ_API}`, qs.stringify(qqApiParam))
|
|
1114
|
-
console.log('QQ私有化api通知结果:', sendResult)
|
|
1005
|
+
console.log('即时消息通知结果:', sendResult)
|
|
1115
1006
|
}
|
|
1116
1007
|
|
|
1117
1008
|
// 即时消息推送内容获取
|
|
1118
|
-
function getIMPushContent (comment
|
|
1009
|
+
function getIMPushContent (comment) {
|
|
1119
1010
|
const SITE_NAME = config.SITE_NAME
|
|
1120
1011
|
const NICK = comment.nick
|
|
1121
1012
|
const MAIL = comment.mail
|
|
@@ -1124,17 +1015,13 @@ function getIMPushContent (comment, { withUrl = true, markdown = false, html = f
|
|
|
1124
1015
|
const SITE_URL = config.SITE_URL
|
|
1125
1016
|
const POST_URL = appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
|
|
1126
1017
|
const subject = config.MAIL_SUBJECT_ADMIN || `${SITE_NAME}有新评论了`
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
}
|
|
1135
|
-
if (markdown) {
|
|
1136
|
-
content = content.replace(/<br>/g, '\n\n')
|
|
1137
|
-
}
|
|
1018
|
+
const content = `评论人:${NICK} ([${MAIL}](mailto:${MAIL}))
|
|
1019
|
+
|
|
1020
|
+
评论人IP:${IP}
|
|
1021
|
+
|
|
1022
|
+
评论内容:${COMMENT}
|
|
1023
|
+
|
|
1024
|
+
原文链接:[${POST_URL}](${POST_URL})`
|
|
1138
1025
|
return {
|
|
1139
1026
|
subject,
|
|
1140
1027
|
content
|
|
@@ -1251,7 +1138,8 @@ async function parse (comment) {
|
|
|
1251
1138
|
// 限流
|
|
1252
1139
|
async function limitFilter () {
|
|
1253
1140
|
// 限制每个 IP 每 10 分钟发表的评论数量
|
|
1254
|
-
|
|
1141
|
+
let limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
|
|
1142
|
+
if (Number.isNaN(limitPerMinute)) limitPerMinute = 10
|
|
1255
1143
|
if (limitPerMinute) {
|
|
1256
1144
|
const count = await db
|
|
1257
1145
|
.collection('comment')
|
|
@@ -1264,7 +1152,8 @@ async function limitFilter () {
|
|
|
1264
1152
|
}
|
|
1265
1153
|
}
|
|
1266
1154
|
// 限制所有 IP 每 10 分钟发表的评论数量
|
|
1267
|
-
|
|
1155
|
+
let limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
|
|
1156
|
+
if (Number.isNaN(limitPerMinuteAll)) limitPerMinuteAll = 10
|
|
1268
1157
|
if (limitPerMinuteAll) {
|
|
1269
1158
|
const count = await db
|
|
1270
1159
|
.collection('comment')
|
|
@@ -1518,6 +1407,41 @@ async function emailTest (event) {
|
|
|
1518
1407
|
return res
|
|
1519
1408
|
}
|
|
1520
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
|
+
|
|
1521
1445
|
function getAvatar (comment) {
|
|
1522
1446
|
if (comment.avatar) {
|
|
1523
1447
|
return comment.avatar
|
|
@@ -1568,7 +1492,6 @@ async function getConfig () {
|
|
|
1568
1492
|
DEFAULT_GRAVATAR: config.DEFAULT_GRAVATAR,
|
|
1569
1493
|
SHOW_IMAGE: config.SHOW_IMAGE || 'true',
|
|
1570
1494
|
IMAGE_CDN: config.IMAGE_CDN,
|
|
1571
|
-
IMAGE_CDN_TOKEN: config.IMAGE_CDN_TOKEN,
|
|
1572
1495
|
SHOW_EMOTION: config.SHOW_EMOTION || 'true',
|
|
1573
1496
|
EMOTION_CDN: config.EMOTION_CDN,
|
|
1574
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"
|