11/*!
2- * Twikoo cloudbase function v1.4.18
2+ * Twikoo cloudbase function v1.5.0
33 * (c) 2020-present iMaeGoo
44 * Released under the MIT License.
55 */
@@ -10,7 +10,6 @@ const md5 = require('blueimp-md5') // MD5 加解密
1010const bowser = require ( 'bowser' ) // UserAgent 格式化
1111const nodemailer = require ( 'nodemailer' ) // 发送邮件
1212const axios = require ( 'axios' ) // 发送 REST 请求
13- const qs = require ( 'querystring' ) // URL 参数格式化
1413const $ = require ( 'cheerio' ) // jQuery 服务器版
1514const { AkismetClient } = require ( 'akismet-api' ) // 反垃圾 API
1615const createDOMPurify = require ( 'dompurify' ) // 反 XSS
@@ -19,6 +18,9 @@ const xml2js = require('xml2js') // XML 解析
1918const marked = require ( 'marked' ) // Markdown 解析
2019const CryptoJS = require ( 'crypto-js' ) // 编解码
2120const 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
2224
2325// 云函数 SDK / tencent cloudbase sdk
2426const app = tcb . init ( { env : tcb . SYMBOL_CURRENT_ENV } )
@@ -31,7 +33,7 @@ const window = new JSDOM('').window
3133const DOMPurify = createDOMPurify ( window )
3234
3335// 常量 / constants
34- const VERSION = '1.4.18 '
36+ const VERSION = '1.5.0 '
3537const RES_CODE = {
3638 SUCCESS : 0 ,
3739 FAIL : 1000 ,
@@ -44,7 +46,8 @@ const RES_CODE = {
4446 PASS_NOT_MATCH : 1023 ,
4547 NEED_LOGIN : 1024 ,
4648 FORBIDDEN : 1403 ,
47- AKISMET_ERROR : 1030
49+ AKISMET_ERROR : 1030 ,
50+ UPLOAD_FAILED : 1040
4851}
4952const ADMIN_USER_ID = 'admin'
5053
@@ -119,6 +122,9 @@ exports.main = async (event, context) => {
119122 case 'EMAIL_TEST' : // >= 1.4.6
120123 res = await emailTest ( event )
121124 break
125+ case 'UPLOAD_IMAGE' : // >= 1.5.0
126+ res = await uploadImage ( event )
127+ break
122128 default :
123129 if ( event . event ) {
124130 res . code = RES_CODE . EVENT_NOT_EXIST
@@ -876,15 +882,9 @@ async function sendNotice (comment) {
876882 await Promise . all ( [
877883 noticeMaster ( comment ) ,
878884 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 )
886886 ] ) . catch ( err => {
887- console . error ( '邮件通知异常 :' , err )
887+ console . error ( '通知异常 :' , err )
888888 } )
889889}
890890
@@ -928,16 +928,8 @@ async function initMailer ({ throwErr = false } = {}) {
928928async function noticeMaster ( comment ) {
929929 if ( ! transporter ) if ( ! await initMailer ( ) ) return
930930 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- ]
939931 // 判断是否存在即时消息推送配置
940- const hasIMPushConfig = IM_PUSH_CONFIGS . some ( item => ! ! config [ item ] )
932+ const hasIMPushConfig = config . PUSHOO_CHANNEL && config . PUSHOO_TOKEN
941933 // 存在即时消息推送配置,则默认不发送邮件给博主
942934 if ( hasIMPushConfig && config . SC_MAIL_NOTIFY !== 'true' ) return
943935 const SITE_NAME = config . SITE_NAME
@@ -986,125 +978,24 @@ async function noticeMaster (comment) {
986978 return sendResult
987979}
988980
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,放弃即时消息通知 ' )
993985 return
994986 }
995987 if ( config . BLOGGER_EMAIL === comment . mail ) return
996988 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 ,
1027991 title : pushContent . subject ,
1028992 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
1087993 } )
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 )
1104995}
1105996
1106997// 即时消息推送内容获取
1107- function getIMPushContent ( comment , { withUrl = true , markdown = false , html = false } = { } ) {
998+ function getIMPushContent ( comment ) {
1108999 const SITE_NAME = config . SITE_NAME
11091000 const NICK = comment . nick
11101001 const MAIL = comment . mail
@@ -1113,17 +1004,13 @@ function getIMPushContent (comment, { withUrl = true, markdown = false, html = f
11131004 const SITE_URL = config . SITE_URL
11141005 const POST_URL = appendHashToUrl ( comment . href || SITE_URL + comment . url , comment . id )
11151006 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 } )`
11271014 return {
11281015 subject,
11291016 content
@@ -1241,7 +1128,8 @@ async function parse (comment) {
12411128// 限流
12421129async function limitFilter ( ) {
12431130 // 限制每个 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
12451133 if ( limitPerMinute ) {
12461134 let count = await db
12471135 . collection ( 'comment' )
@@ -1256,7 +1144,8 @@ async function limitFilter () {
12561144 }
12571145 }
12581146 // 限制所有 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
12601149 if ( limitPerMinuteAll ) {
12611150 let count = await db
12621151 . collection ( 'comment' )
@@ -1511,6 +1400,41 @@ async function emailTest (event) {
15111400 return res
15121401}
15131402
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+
15141438function getAvatar ( comment ) {
15151439 if ( comment . avatar ) {
15161440 return comment . avatar
0 commit comments