twikoo-func 1.4.18 → 1.5.2

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 +100 -147
  2. package/package.json +3 -2
package/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  /*!
2
- * Twikoo cloudbase function v1.4.18
2
+ * Twikoo cloudbase function
3
3
  * (c) 2020-present iMaeGoo
4
4
  * Released under the MIT License.
5
5
  */
6
6
 
7
7
  // 三方依赖 / 3rd party dependencies
8
+ const { version: VERSION } = require('./package.json')
8
9
  const tcb = require('@cloudbase/node-sdk') // 云开发 SDK
9
10
  const md5 = require('blueimp-md5') // MD5 加解密
10
11
  const bowser = require('bowser') // UserAgent 格式化
11
12
  const nodemailer = require('nodemailer') // 发送邮件
12
13
  const axios = require('axios') // 发送 REST 请求
13
- const qs = require('querystring') // URL 参数格式化
14
14
  const $ = require('cheerio') // jQuery 服务器版
15
15
  const { AkismetClient } = require('akismet-api') // 反垃圾 API
16
16
  const createDOMPurify = require('dompurify') // 反 XSS
@@ -19,6 +19,9 @@ const xml2js = require('xml2js') // XML 解析
19
19
  const marked = require('marked') // Markdown 解析
20
20
  const CryptoJS = require('crypto-js') // 编解码
21
21
  const tencentcloud = require('tencentcloud-sdk-nodejs') // 腾讯云 API NODEJS SDK
22
+ const fs = require('fs')
23
+ const FormData = require('form-data') // 图片上传
24
+ const pushoo = require('pushoo').default
22
25
 
23
26
  // 云函数 SDK / tencent cloudbase sdk
24
27
  const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })
@@ -31,7 +34,6 @@ const window = new JSDOM('').window
31
34
  const DOMPurify = createDOMPurify(window)
32
35
 
33
36
  // 常量 / constants
34
- const VERSION = '1.4.18'
35
37
  const RES_CODE = {
36
38
  SUCCESS: 0,
37
39
  FAIL: 1000,
@@ -44,23 +46,28 @@ 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'
53
+ const MAX_REQUEST_TIMES = parseInt(process.env.TWIKOO_THROTTLE) || 250
50
54
 
51
55
  // 全局变量 / variables
52
56
  // 警告:全局定义的变量,会被云函数缓存,请慎重定义全局变量
53
57
  // 参考 https://docs.cloudbase.net/cloud-function/deep-principle.html 中的 “实例复用”
54
58
  let config
55
59
  let transporter
60
+ const requestTimes = {}
56
61
 
57
62
  // 云函数入口点 / entry point
58
63
  exports.main = async (event, context) => {
64
+ console.log('请求IP:', auth.getClientIP())
59
65
  console.log('请求方法:', event.event)
60
66
  console.log('请求参数:', event)
61
67
  let res = {}
62
- await readConfig()
63
68
  try {
69
+ protect()
70
+ await readConfig()
64
71
  switch (event.event) {
65
72
  case 'GET_FUNC_VERSION':
66
73
  res = getFuncVersion()
@@ -119,6 +126,9 @@ exports.main = async (event, context) => {
119
126
  case 'EMAIL_TEST': // >= 1.4.6
120
127
  res = await emailTest(event)
121
128
  break
129
+ case 'UPLOAD_IMAGE': // >= 1.5.0
130
+ res = await uploadImage(event)
131
+ break
122
132
  default:
123
133
  if (event.event) {
124
134
  res.code = RES_CODE.EVENT_NOT_EXIST
@@ -876,15 +886,9 @@ async function sendNotice (comment) {
876
886
  await Promise.all([
877
887
  noticeMaster(comment),
878
888
  noticeReply(comment),
879
- noticeWeChat(comment),
880
- noticePushPlus(comment),
881
- noticeWeComPush(comment),
882
- noticeDingTalkHook(comment),
883
- noticePushdeer(comment),
884
- noticeQQ(comment),
885
- noticeQQAPI(comment)
889
+ noticePushoo(comment)
886
890
  ]).catch(err => {
887
- console.error('邮件通知异常:', err)
891
+ console.error('通知异常:', err)
888
892
  })
889
893
  }
890
894
 
@@ -928,16 +932,8 @@ async function initMailer ({ throwErr = false } = {}) {
928
932
  async function noticeMaster (comment) {
929
933
  if (!transporter) if (!await initMailer()) return
930
934
  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
935
  // 判断是否存在即时消息推送配置
940
- const hasIMPushConfig = IM_PUSH_CONFIGS.some(item => !!config[item])
936
+ const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
941
937
  // 存在即时消息推送配置,则默认不发送邮件给博主
942
938
  if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') return
943
939
  const SITE_NAME = config.SITE_NAME
@@ -986,125 +982,29 @@ async function noticeMaster (comment) {
986
982
  return sendResult
987
983
  }
988
984
 
989
- // 微信通知
990
- async function noticeWeChat (comment) {
991
- if (!config.SC_SENDKEY) {
992
- console.log('没有配置 server 酱,放弃微信通知')
985
+ // 即时消息通知
986
+ async function noticePushoo (comment) {
987
+ if (!config.PUSHOO_CHANNEL || !config.PUSHOO_TOKEN) {
988
+ console.log('没有配置 pushoo,放弃即时消息通知')
993
989
  return
994
990
  }
995
991
  if (config.BLOGGER_EMAIL === comment.mail) return
996
992
  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,
993
+ const sendResult = await pushoo(config.PUSHOO_CHANNEL, {
994
+ token: config.PUSHOO_TOKEN,
1027
995
  title: pushContent.subject,
1028
- 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
996
+ content: pushContent.content,
997
+ options: {
998
+ bark: {
999
+ url: pushContent.url
1000
+ }
1001
+ }
1087
1002
  })
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)
1003
+ console.log('即时消息通知结果:', sendResult)
1104
1004
  }
1105
1005
 
1106
1006
  // 即时消息推送内容获取
1107
- function getIMPushContent (comment, { withUrl = true, markdown = false, html = false } = {}) {
1007
+ function getIMPushContent (comment) {
1108
1008
  const SITE_NAME = config.SITE_NAME
1109
1009
  const NICK = comment.nick
1110
1010
  const MAIL = comment.mail
@@ -1113,20 +1013,17 @@ function getIMPushContent (comment, { withUrl = true, markdown = false, html = f
1113
1013
  const SITE_URL = config.SITE_URL
1114
1014
  const POST_URL = appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
1115
1015
  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
- }
1016
+ const content = `评论人:${NICK} ([${MAIL}](mailto:${MAIL}))
1017
+
1018
+ 评论人IP:${IP}
1019
+
1020
+ 评论内容:${COMMENT}
1021
+
1022
+ 原文链接:[${POST_URL}](${POST_URL})`
1127
1023
  return {
1128
1024
  subject,
1129
- content
1025
+ content,
1026
+ url: POST_URL
1130
1027
  }
1131
1028
  }
1132
1029
 
@@ -1241,7 +1138,8 @@ async function parse (comment) {
1241
1138
  // 限流
1242
1139
  async function limitFilter () {
1243
1140
  // 限制每个 IP 每 10 分钟发表的评论数量
1244
- const limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
1141
+ let limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
1142
+ if (Number.isNaN(limitPerMinute)) limitPerMinute = 10
1245
1143
  if (limitPerMinute) {
1246
1144
  let count = await db
1247
1145
  .collection('comment')
@@ -1256,7 +1154,8 @@ async function limitFilter () {
1256
1154
  }
1257
1155
  }
1258
1156
  // 限制所有 IP 每 10 分钟发表的评论数量
1259
- const limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
1157
+ let limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
1158
+ if (Number.isNaN(limitPerMinuteAll)) limitPerMinuteAll = 10
1260
1159
  if (limitPerMinuteAll) {
1261
1160
  let count = await db
1262
1161
  .collection('comment')
@@ -1273,6 +1172,12 @@ async function limitFilter () {
1273
1172
 
1274
1173
  // 预垃圾评论检测
1275
1174
  function preCheckSpam (comment) {
1175
+ // 长度限制
1176
+ let limitLength = parseInt(config.LIMIT_LENGTH)
1177
+ if (Number.isNaN(limitLength)) limitLength = 500
1178
+ if (limitLength && comment.length > limitLength) {
1179
+ throw new Error('评论内容过长')
1180
+ }
1276
1181
  if (config.AKISMET_KEY === 'MANUAL_REVIEW') {
1277
1182
  // 人工审核
1278
1183
  console.log('已使用人工审核模式,评论审核后才会发表~')
@@ -1511,6 +1416,41 @@ async function emailTest (event) {
1511
1416
  return res
1512
1417
  }
1513
1418
 
1419
+ async function uploadImage (event) {
1420
+ const { photo, fileName } = event
1421
+ const res = {}
1422
+ try {
1423
+ if (!config.IMAGE_CDN_TOKEN) {
1424
+ throw new Error('未配置图片上传服务')
1425
+ }
1426
+ const formData = new FormData()
1427
+ formData.append('image', base64UrlToReadStream(photo, fileName))
1428
+ const uploadResult = await axios.post('https://7bu.top/api/upload', formData, {
1429
+ headers: {
1430
+ ...formData.getHeaders(),
1431
+ token: config.IMAGE_CDN_TOKEN
1432
+ }
1433
+ })
1434
+ if (uploadResult.data.code === 200) {
1435
+ res.data = uploadResult.data.data
1436
+ } else {
1437
+ throw new Error(uploadResult.data.msg)
1438
+ }
1439
+ } catch (e) {
1440
+ console.error(e)
1441
+ res.code = RES_CODE.UPLOAD_FAILED
1442
+ res.err = e.message
1443
+ }
1444
+ return res
1445
+ }
1446
+
1447
+ function base64UrlToReadStream (base64Url, fileName) {
1448
+ const base64 = base64Url.split(';base64,').pop()
1449
+ const path = `/tmp/${fileName}`
1450
+ fs.writeFileSync(path, base64, { encoding: 'base64' })
1451
+ return fs.createReadStream(path)
1452
+ }
1453
+
1514
1454
  function getAvatar (comment) {
1515
1455
  if (comment.avatar) {
1516
1456
  return comment.avatar
@@ -1567,7 +1507,8 @@ function getConfig () {
1567
1507
  REQUIRED_FIELDS: config.REQUIRED_FIELDS,
1568
1508
  HIDE_ADMIN_CRYPT: config.HIDE_ADMIN_CRYPT,
1569
1509
  HIGHLIGHT: config.HIGHLIGHT || 'true',
1570
- HIGHLIGHT_THEME: config.HIGHLIGHT_THEME
1510
+ HIGHLIGHT_THEME: config.HIGHLIGHT_THEME,
1511
+ LIMIT_LENGTH: config.LIMIT_LENGTH
1571
1512
  }
1572
1513
  }
1573
1514
  }
@@ -1604,6 +1545,18 @@ async function setConfig (event) {
1604
1545
  }
1605
1546
  }
1606
1547
 
1548
+ function protect () {
1549
+ // 防御
1550
+ const ip = auth.getClientIP()
1551
+ requestTimes[ip] = (requestTimes[ip] || 0) + 1
1552
+ if (requestTimes[ip] > MAX_REQUEST_TIMES) {
1553
+ console.log(`${ip} 当前请求次数为 ${requestTimes[ip]},已超过最大请求次数`)
1554
+ throw new Error('Too Many Requests')
1555
+ } else {
1556
+ console.log(`${ip} 当前请求次数为 ${requestTimes[ip]}`)
1557
+ }
1558
+ }
1559
+
1607
1560
  // 读取配置
1608
1561
  async function readConfig () {
1609
1562
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twikoo-func",
3
- "version": "1.4.18",
3
+ "version": "1.5.2",
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
  }