1
1
/*!
2
- * Twikoo cloudbase function v1.4.18
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.4.18 '
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
- noticeWeChat ( comment ) ,
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 ( '邮件通知异常 :' , err )
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 = IM_PUSH_CONFIGS . some ( item => ! ! config [ item ] )
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 noticeWeChat ( comment ) {
991
- if ( ! config . SC_SENDKEY ) {
992
- console . log ( '没有配置 server 酱,放弃微信通知 ' )
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
- let scApiUrl = 'https://sc.ftqq.com'
998
- let scApiParam = {
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 ( / < b r > / 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 ( 'Pushdeer 通知结果:' , sendResult )
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 ( / < b r > / 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 , { withUrl = true , markdown = false , html = false } = { } ) {
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
- let content = `评论人:${ NICK } (${ MAIL } )<br>评论人IP:${ IP } <br>评论内容:${ COMMENT } <br>`
1117
- // Qmsg 会过滤带网址的推送消息,所以不能带网址
1118
- if ( withUrl ) {
1119
- content += `原文链接:${ markdown ? `[${ POST_URL } ](${ POST_URL } )` : POST_URL } `
1120
- }
1121
- if ( html ) {
1122
- content += `原文链接:<a href="${ POST_URL } " rel="nofollow">${ POST_URL } </a>`
1123
- }
1124
- if ( markdown ) {
1125
- content = content . replace ( / < b r > / 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
- const limitPerMinute = parseInt ( config . LIMIT_PER_MINUTE )
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
- const limitPerMinuteAll = parseInt ( config . LIMIT_PER_MINUTE_ALL )
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
0 commit comments