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.
Files changed (2) hide show
  1. package/index.js +67 -143
  2. package/package.json +3 -2
package/index.js CHANGED
@@ -1,5 +1,5 @@
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(/<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('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(/<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, { 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(/<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
- 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twikoo-func",
3
- "version": "1.4.18",
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
- "querystring": "^0.2.0",
27
+ "pushoo": "latest",
27
28
  "tencentcloud-sdk-nodejs": "^4.0.65",
28
29
  "xml2js": "^0.4.23"
29
30
  }