twikoo-func 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/index.js +67 -143
- package/package.json +3 -2
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Twikoo cloudbase function v1.
|
|
2
|
+
* Twikoo cloudbase function v1.5.0
|
|
3
3
|
* (c) 2020-present iMaeGoo
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
*/
|
|
@@ -10,7 +10,6 @@ const md5 = require('blueimp-md5') // MD5 加解密
|
|
|
10
10
|
const bowser = require('bowser') // UserAgent 格式化
|
|
11
11
|
const nodemailer = require('nodemailer') // 发送邮件
|
|
12
12
|
const axios = require('axios') // 发送 REST 请求
|
|
13
|
-
const qs = require('querystring') // URL 参数格式化
|
|
14
13
|
const $ = require('cheerio') // jQuery 服务器版
|
|
15
14
|
const { AkismetClient } = require('akismet-api') // 反垃圾 API
|
|
16
15
|
const createDOMPurify = require('dompurify') // 反 XSS
|
|
@@ -19,6 +18,9 @@ const xml2js = require('xml2js') // XML 解析
|
|
|
19
18
|
const marked = require('marked') // Markdown 解析
|
|
20
19
|
const CryptoJS = require('crypto-js') // 编解码
|
|
21
20
|
const tencentcloud = require('tencentcloud-sdk-nodejs') // 腾讯云 API NODEJS SDK
|
|
21
|
+
const fs = require('fs')
|
|
22
|
+
const FormData = require('form-data') // 图片上传
|
|
23
|
+
const pushoo = require('pushoo').default
|
|
22
24
|
|
|
23
25
|
// 云函数 SDK / tencent cloudbase sdk
|
|
24
26
|
const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })
|
|
@@ -31,7 +33,7 @@ const window = new JSDOM('').window
|
|
|
31
33
|
const DOMPurify = createDOMPurify(window)
|
|
32
34
|
|
|
33
35
|
// 常量 / constants
|
|
34
|
-
const VERSION = '1.
|
|
36
|
+
const VERSION = '1.5.0'
|
|
35
37
|
const RES_CODE = {
|
|
36
38
|
SUCCESS: 0,
|
|
37
39
|
FAIL: 1000,
|
|
@@ -44,7 +46,8 @@ const RES_CODE = {
|
|
|
44
46
|
PASS_NOT_MATCH: 1023,
|
|
45
47
|
NEED_LOGIN: 1024,
|
|
46
48
|
FORBIDDEN: 1403,
|
|
47
|
-
AKISMET_ERROR: 1030
|
|
49
|
+
AKISMET_ERROR: 1030,
|
|
50
|
+
UPLOAD_FAILED: 1040
|
|
48
51
|
}
|
|
49
52
|
const ADMIN_USER_ID = 'admin'
|
|
50
53
|
|
|
@@ -119,6 +122,9 @@ exports.main = async (event, context) => {
|
|
|
119
122
|
case 'EMAIL_TEST': // >= 1.4.6
|
|
120
123
|
res = await emailTest(event)
|
|
121
124
|
break
|
|
125
|
+
case 'UPLOAD_IMAGE': // >= 1.5.0
|
|
126
|
+
res = await uploadImage(event)
|
|
127
|
+
break
|
|
122
128
|
default:
|
|
123
129
|
if (event.event) {
|
|
124
130
|
res.code = RES_CODE.EVENT_NOT_EXIST
|
|
@@ -876,15 +882,9 @@ async function sendNotice (comment) {
|
|
|
876
882
|
await Promise.all([
|
|
877
883
|
noticeMaster(comment),
|
|
878
884
|
noticeReply(comment),
|
|
879
|
-
|
|
880
|
-
noticePushPlus(comment),
|
|
881
|
-
noticeWeComPush(comment),
|
|
882
|
-
noticeDingTalkHook(comment),
|
|
883
|
-
noticePushdeer(comment),
|
|
884
|
-
noticeQQ(comment),
|
|
885
|
-
noticeQQAPI(comment)
|
|
885
|
+
noticePushoo(comment)
|
|
886
886
|
]).catch(err => {
|
|
887
|
-
console.error('
|
|
887
|
+
console.error('通知异常:', err)
|
|
888
888
|
})
|
|
889
889
|
}
|
|
890
890
|
|
|
@@ -928,16 +928,8 @@ async function initMailer ({ throwErr = false } = {}) {
|
|
|
928
928
|
async function noticeMaster (comment) {
|
|
929
929
|
if (!transporter) if (!await initMailer()) return
|
|
930
930
|
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
931
|
-
const IM_PUSH_CONFIGS = [
|
|
932
|
-
'SC_SENDKEY',
|
|
933
|
-
'QM_SENDKEY',
|
|
934
|
-
'PUSH_PLUS_TOKEN',
|
|
935
|
-
'WECOM_API_URL',
|
|
936
|
-
'DINGTALK_WEBHOOK_URL',
|
|
937
|
-
'PUSHDEER_KEY'
|
|
938
|
-
]
|
|
939
931
|
// 判断是否存在即时消息推送配置
|
|
940
|
-
const hasIMPushConfig =
|
|
932
|
+
const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
|
|
941
933
|
// 存在即时消息推送配置,则默认不发送邮件给博主
|
|
942
934
|
if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') return
|
|
943
935
|
const SITE_NAME = config.SITE_NAME
|
|
@@ -986,125 +978,24 @@ async function noticeMaster (comment) {
|
|
|
986
978
|
return sendResult
|
|
987
979
|
}
|
|
988
980
|
|
|
989
|
-
//
|
|
990
|
-
async function
|
|
991
|
-
if (!config.
|
|
992
|
-
console.log('没有配置
|
|
981
|
+
// 即时消息通知
|
|
982
|
+
async function noticePushoo (comment) {
|
|
983
|
+
if (!config.PUSHOO_CHANNEL || !config.PUSHOO_TOKEN) {
|
|
984
|
+
console.log('没有配置 pushoo,放弃即时消息通知')
|
|
993
985
|
return
|
|
994
986
|
}
|
|
995
987
|
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
996
988
|
const pushContent = getIMPushContent(comment)
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
text: pushContent.subject,
|
|
1000
|
-
desp: pushContent.content
|
|
1001
|
-
}
|
|
1002
|
-
if (config.SC_SENDKEY.substring(0, 3).toLowerCase() === 'sct') {
|
|
1003
|
-
// 兼容 server 酱测试专版
|
|
1004
|
-
scApiUrl = 'https://sctapi.ftqq.com'
|
|
1005
|
-
scApiParam = {
|
|
1006
|
-
title: pushContent.subject,
|
|
1007
|
-
desp: pushContent.content
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
const sendResult = await axios.post(`${scApiUrl}/${config.SC_SENDKEY}.send`, qs.stringify(scApiParam), {
|
|
1011
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
1012
|
-
})
|
|
1013
|
-
console.log('微信通知结果:', sendResult)
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
// pushplus 通知
|
|
1017
|
-
async function noticePushPlus (comment) {
|
|
1018
|
-
if (!config.PUSH_PLUS_TOKEN) {
|
|
1019
|
-
console.log('没有配置 pushplus,放弃通知')
|
|
1020
|
-
return
|
|
1021
|
-
}
|
|
1022
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1023
|
-
const pushContent = getIMPushContent(comment, { withUrl: false, html: true })
|
|
1024
|
-
const ppApiUrl = 'http://pushplus.hxtrip.com/send'
|
|
1025
|
-
const ppApiParam = {
|
|
1026
|
-
token: config.PUSH_PLUS_TOKEN,
|
|
989
|
+
const sendResult = await pushoo(config.PUSHOO_CHANNEL, {
|
|
990
|
+
token: config.PUSHOO_TOKEN,
|
|
1027
991
|
title: pushContent.subject,
|
|
1028
992
|
content: pushContent.content
|
|
1029
|
-
}
|
|
1030
|
-
const sendResult = await axios.post(ppApiUrl, ppApiParam)
|
|
1031
|
-
console.log('pushplus 通知结果:', sendResult)
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// 自定义WeCom企业微信api通知
|
|
1035
|
-
async function noticeWeComPush (comment) {
|
|
1036
|
-
if (!config.WECOM_API_URL) {
|
|
1037
|
-
console.log('未配置 WECOM_API_URL,跳过企业微信推送')
|
|
1038
|
-
return
|
|
1039
|
-
}
|
|
1040
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1041
|
-
const SITE_URL = config.SITE_URL
|
|
1042
|
-
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)
|
|
1043
|
-
const WeComApiContent = encodeURIComponent(WeComContent)
|
|
1044
|
-
const WeComApiUrl = config.WECOM_API_URL
|
|
1045
|
-
const sendResult = await axios.get(WeComApiUrl + WeComApiContent)
|
|
1046
|
-
console.log('WinxinPush 通知结果:', sendResult)
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
// 自定义钉钉WebHook通知
|
|
1050
|
-
async function noticeDingTalkHook (comment) {
|
|
1051
|
-
if (!config.DINGTALK_WEBHOOK_URL) {
|
|
1052
|
-
console.log('没有配置 DingTalk_WebHook,放弃钉钉WebHook推送')
|
|
1053
|
-
return
|
|
1054
|
-
}
|
|
1055
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1056
|
-
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)
|
|
1057
|
-
const sendResult = await axios.post(config.DINGTALK_WEBHOOK_URL, { msgtype: 'text', text: { content: DingTalkContent } })
|
|
1058
|
-
console.log('钉钉WebHook 通知结果:', sendResult)
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// QQ通知
|
|
1062
|
-
async function noticeQQ (comment) {
|
|
1063
|
-
if (!config.QM_SENDKEY) {
|
|
1064
|
-
console.log('没有配置 qmsg 酱,放弃QQ通知')
|
|
1065
|
-
return
|
|
1066
|
-
}
|
|
1067
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1068
|
-
const pushContent = getIMPushContent(comment, { withUrl: false })
|
|
1069
|
-
const qmApiUrl = 'https://qmsg.zendee.cn'
|
|
1070
|
-
const qmApiParam = {
|
|
1071
|
-
msg: pushContent.subject + '\n' + pushContent.content.replace(/<br>/g, '\n')
|
|
1072
|
-
}
|
|
1073
|
-
const sendResult = await axios.post(`${qmApiUrl}/send/${config.QM_SENDKEY}`, qs.stringify(qmApiParam), {
|
|
1074
|
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
1075
|
-
})
|
|
1076
|
-
console.log('QQ通知结果:', sendResult)
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
async function noticePushdeer (comment) {
|
|
1080
|
-
if (!config.PUSHDEER_KEY) return
|
|
1081
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1082
|
-
const pushContent = getIMPushContent(comment, { markdown: true })
|
|
1083
|
-
const sendResult = await axios.post('https://api2.pushdeer.com/message/push', {
|
|
1084
|
-
pushkey: config.PUSHDEER_KEY,
|
|
1085
|
-
text: pushContent.subject,
|
|
1086
|
-
desp: pushContent.content
|
|
1087
993
|
})
|
|
1088
|
-
console.log('
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// QQ私有化API通知
|
|
1092
|
-
async function noticeQQAPI (comment) {
|
|
1093
|
-
if (!config.QQ_API) {
|
|
1094
|
-
console.log('没有配置QQ私有化api,放弃QQ通知')
|
|
1095
|
-
return
|
|
1096
|
-
}
|
|
1097
|
-
if (config.BLOGGER_EMAIL === comment.mail) return
|
|
1098
|
-
const pushContent = getIMPushContent(comment)
|
|
1099
|
-
const qqApiParam = {
|
|
1100
|
-
message: pushContent.subject + '\n' + pushContent.content.replace(/<br>/g, '\n')
|
|
1101
|
-
}
|
|
1102
|
-
const sendResult = await axios.post(`${config.QQ_API}`, qs.stringify(qqApiParam))
|
|
1103
|
-
console.log('QQ私有化api通知结果:', sendResult)
|
|
994
|
+
console.log('即时消息通知结果:', sendResult)
|
|
1104
995
|
}
|
|
1105
996
|
|
|
1106
997
|
// 即时消息推送内容获取
|
|
1107
|
-
function getIMPushContent (comment
|
|
998
|
+
function getIMPushContent (comment) {
|
|
1108
999
|
const SITE_NAME = config.SITE_NAME
|
|
1109
1000
|
const NICK = comment.nick
|
|
1110
1001
|
const MAIL = comment.mail
|
|
@@ -1113,17 +1004,13 @@ function getIMPushContent (comment, { withUrl = true, markdown = false, html = f
|
|
|
1113
1004
|
const SITE_URL = config.SITE_URL
|
|
1114
1005
|
const POST_URL = appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
|
|
1115
1006
|
const subject = config.MAIL_SUBJECT_ADMIN || `${SITE_NAME}有新评论了`
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
}
|
|
1124
|
-
if (markdown) {
|
|
1125
|
-
content = content.replace(/<br>/g, '\n\n')
|
|
1126
|
-
}
|
|
1007
|
+
const content = `评论人:${NICK} ([${MAIL}](mailto:${MAIL}))
|
|
1008
|
+
|
|
1009
|
+
评论人IP:${IP}
|
|
1010
|
+
|
|
1011
|
+
评论内容:${COMMENT}
|
|
1012
|
+
|
|
1013
|
+
原文链接:[${POST_URL}](${POST_URL})`
|
|
1127
1014
|
return {
|
|
1128
1015
|
subject,
|
|
1129
1016
|
content
|
|
@@ -1241,7 +1128,8 @@ async function parse (comment) {
|
|
|
1241
1128
|
// 限流
|
|
1242
1129
|
async function limitFilter () {
|
|
1243
1130
|
// 限制每个 IP 每 10 分钟发表的评论数量
|
|
1244
|
-
|
|
1131
|
+
let limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
|
|
1132
|
+
if (Number.isNaN(limitPerMinute)) limitPerMinute = 10
|
|
1245
1133
|
if (limitPerMinute) {
|
|
1246
1134
|
let count = await db
|
|
1247
1135
|
.collection('comment')
|
|
@@ -1256,7 +1144,8 @@ async function limitFilter () {
|
|
|
1256
1144
|
}
|
|
1257
1145
|
}
|
|
1258
1146
|
// 限制所有 IP 每 10 分钟发表的评论数量
|
|
1259
|
-
|
|
1147
|
+
let limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
|
|
1148
|
+
if (Number.isNaN(limitPerMinuteAll)) limitPerMinuteAll = 10
|
|
1260
1149
|
if (limitPerMinuteAll) {
|
|
1261
1150
|
let count = await db
|
|
1262
1151
|
.collection('comment')
|
|
@@ -1511,6 +1400,41 @@ async function emailTest (event) {
|
|
|
1511
1400
|
return res
|
|
1512
1401
|
}
|
|
1513
1402
|
|
|
1403
|
+
async function uploadImage (event) {
|
|
1404
|
+
const { photo, fileName } = event
|
|
1405
|
+
const res = {}
|
|
1406
|
+
try {
|
|
1407
|
+
if (!config.IMAGE_CDN_TOKEN) {
|
|
1408
|
+
throw new Error('未配置图片上传服务')
|
|
1409
|
+
}
|
|
1410
|
+
const formData = new FormData()
|
|
1411
|
+
formData.append('image', base64UrlToReadStream(photo, fileName))
|
|
1412
|
+
const uploadResult = await axios.post('https://7bu.top/api/upload', formData, {
|
|
1413
|
+
headers: {
|
|
1414
|
+
...formData.getHeaders(),
|
|
1415
|
+
token: config.IMAGE_CDN_TOKEN
|
|
1416
|
+
}
|
|
1417
|
+
})
|
|
1418
|
+
if (uploadResult.data.code === 200) {
|
|
1419
|
+
res.data = uploadResult.data.data
|
|
1420
|
+
} else {
|
|
1421
|
+
throw new Error(uploadResult.data.msg)
|
|
1422
|
+
}
|
|
1423
|
+
} catch (e) {
|
|
1424
|
+
console.error(e)
|
|
1425
|
+
res.code = RES_CODE.UPLOAD_FAILED
|
|
1426
|
+
res.err = e.message
|
|
1427
|
+
}
|
|
1428
|
+
return res
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function base64UrlToReadStream (base64Url, fileName) {
|
|
1432
|
+
const base64 = base64Url.split(';base64,').pop()
|
|
1433
|
+
const path = `/tmp/${fileName}`
|
|
1434
|
+
fs.writeFileSync(path, base64, { encoding: 'base64' })
|
|
1435
|
+
return fs.createReadStream(path)
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1514
1438
|
function getAvatar (comment) {
|
|
1515
1439
|
if (comment.avatar) {
|
|
1516
1440
|
return comment.avatar
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "twikoo-func",
|
|
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",
|
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
"cheerio": "1.0.0-rc.5",
|
|
21
21
|
"crypto-js": "^4.0.0",
|
|
22
22
|
"dompurify": "^2.2.6",
|
|
23
|
+
"form-data": "^4.0.0",
|
|
23
24
|
"jsdom": "^16.4.0",
|
|
24
25
|
"marked": "^4.0.12",
|
|
25
26
|
"nodemailer": "^6.4.17",
|
|
26
|
-
"
|
|
27
|
+
"pushoo": "latest",
|
|
27
28
|
"tencentcloud-sdk-nodejs": "^4.0.65",
|
|
28
29
|
"xml2js": "^0.4.23"
|
|
29
30
|
}
|