twikoo-vercel 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/api/index.js +66 -143
  2. package/package.json +3 -2
package/api/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Twikoo vercel function v1.4.18
2
+ * Twikoo vercel function v1.5.0
3
3
  * (c) 2020-present iMaeGoo
4
4
  * Released under the MIT License.
5
5
  */
@@ -11,7 +11,6 @@ const md5 = require('blueimp-md5') // MD5 加解密
11
11
  const bowser = require('bowser') // UserAgent 格式化
12
12
  const nodemailer = require('nodemailer') // 发送邮件
13
13
  const axios = require('axios') // 发送 REST 请求
14
- const qs = require('querystring') // URL 参数格式化
15
14
  const $ = require('cheerio') // jQuery 服务器版
16
15
  const { AkismetClient } = require('akismet-api') // 反垃圾 API
17
16
  const createDOMPurify = require('dompurify') // 反 XSS
@@ -21,13 +20,16 @@ const marked = require('marked') // Markdown 解析
21
20
  const CryptoJS = require('crypto-js') // 编解码
22
21
  const tencentcloud = require('tencentcloud-sdk-nodejs') // 腾讯云 API NODEJS SDK
23
22
  const { v4: uuidv4 } = require('uuid') // 用户 id 生成
23
+ const fs = require('fs')
24
+ const FormData = require('form-data') // 图片上传
25
+ const pushoo = require('pushoo').default
24
26
 
25
27
  // 初始化反 XSS
26
28
  const window = new JSDOM('').window
27
29
  const DOMPurify = createDOMPurify(window)
28
30
 
29
31
  // 常量 / constants
30
- const VERSION = '1.4.18'
32
+ const VERSION = '1.5.0'
31
33
  const RES_CODE = {
32
34
  SUCCESS: 0,
33
35
  NO_PARAM: 100,
@@ -41,7 +43,8 @@ const RES_CODE = {
41
43
  PASS_NOT_MATCH: 1023,
42
44
  NEED_LOGIN: 1024,
43
45
  FORBIDDEN: 1403,
44
- AKISMET_ERROR: 1030
46
+ AKISMET_ERROR: 1030,
47
+ UPLOAD_FAILED: 1040
45
48
  }
46
49
 
47
50
  // 全局变量 / variables
@@ -126,6 +129,9 @@ module.exports = async (requestArg, responseArg) => {
126
129
  case 'EMAIL_TEST': // >= 1.4.6
127
130
  res = await emailTest(event)
128
131
  break
132
+ case 'UPLOAD_IMAGE': // >= 1.5.0
133
+ res = await uploadImage(event)
134
+ break
129
135
  default:
130
136
  if (event.event) {
131
137
  res.code = RES_CODE.EVENT_NOT_EXIST
@@ -888,13 +894,7 @@ async function sendNotice (comment) {
888
894
  await Promise.all([
889
895
  noticeMaster(comment),
890
896
  noticeReply(comment),
891
- noticeWeChat(comment),
892
- noticePushPlus(comment),
893
- noticeWeComPush(comment),
894
- noticeDingTalkHook(comment),
895
- noticePushdeer(comment),
896
- noticeQQ(comment),
897
- noticeQQAPI(comment)
897
+ noticePushoo(comment)
898
898
  ]).catch(console.error)
899
899
  return { code: RES_CODE.SUCCESS }
900
900
  }
@@ -939,16 +939,8 @@ async function initMailer ({ throwErr = false } = {}) {
939
939
  async function noticeMaster (comment) {
940
940
  if (!transporter) if (!await initMailer()) return
941
941
  if (config.BLOGGER_EMAIL === comment.mail) return
942
- const IM_PUSH_CONFIGS = [
943
- 'SC_SENDKEY',
944
- 'QM_SENDKEY',
945
- 'PUSH_PLUS_TOKEN',
946
- 'WECOM_API_URL',
947
- 'DINGTALK_WEBHOOK_URL',
948
- 'PUSHDEER_KEY'
949
- ]
950
942
  // 判断是否存在即时消息推送配置
951
- const hasIMPushConfig = IM_PUSH_CONFIGS.some(item => !!config[item])
943
+ const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
952
944
  // 存在即时消息推送配置,则默认不发送邮件给博主
953
945
  if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') return
954
946
  const SITE_NAME = config.SITE_NAME
@@ -997,125 +989,24 @@ async function noticeMaster (comment) {
997
989
  return sendResult
998
990
  }
999
991
 
1000
- // 微信通知
1001
- async function noticeWeChat (comment) {
1002
- if (!config.SC_SENDKEY) {
1003
- console.log('没有配置 server 酱,放弃微信通知')
992
+ // 即时消息通知
993
+ async function noticePushoo (comment) {
994
+ if (!config.PUSHOO_CHANNEL || !config.PUSHOO_TOKEN) {
995
+ console.log('没有配置 pushoo,放弃即时消息通知')
1004
996
  return
1005
997
  }
1006
998
  if (config.BLOGGER_EMAIL === comment.mail) return
1007
999
  const pushContent = getIMPushContent(comment)
1008
- let scApiUrl = 'https://sc.ftqq.com'
1009
- let scApiParam = {
1010
- text: pushContent.subject,
1011
- desp: pushContent.content
1012
- }
1013
- if (config.SC_SENDKEY.substring(0, 3).toLowerCase() === 'sct') {
1014
- // 兼容 server 酱测试专版
1015
- scApiUrl = 'https://sctapi.ftqq.com'
1016
- scApiParam = {
1017
- title: pushContent.subject,
1018
- desp: pushContent.content
1019
- }
1020
- }
1021
- const sendResult = await axios.post(`${scApiUrl}/${config.SC_SENDKEY}.send`, qs.stringify(scApiParam), {
1022
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
1023
- })
1024
- console.log('微信通知结果:', sendResult)
1025
- }
1026
-
1027
- // pushplus 通知
1028
- async function noticePushPlus (comment) {
1029
- if (!config.PUSH_PLUS_TOKEN) {
1030
- console.log('没有配置 pushplus,放弃通知')
1031
- return
1032
- }
1033
- if (config.BLOGGER_EMAIL === comment.mail) return
1034
- const pushContent = getIMPushContent(comment)
1035
- const ppApiUrl = 'http://pushplus.hxtrip.com/send'
1036
- const ppApiParam = {
1037
- token: config.PUSH_PLUS_TOKEN,
1000
+ const sendResult = await pushoo(config.PUSHOO_CHANNEL, {
1001
+ token: config.PUSHOO_TOKEN,
1038
1002
  title: pushContent.subject,
1039
1003
  content: pushContent.content
1040
- }
1041
- const sendResult = await axios.post(ppApiUrl, ppApiParam)
1042
- console.log('pushplus 通知结果:', sendResult)
1043
- }
1044
-
1045
- // 自定义WeCom企业微信api通知
1046
- async function noticeWeComPush (comment) {
1047
- if (!config.WECOM_API_URL) {
1048
- console.log('未配置 WECOM_API_URL,跳过企业微信推送')
1049
- return
1050
- }
1051
- if (config.BLOGGER_EMAIL === comment.mail) return
1052
- const SITE_URL = config.SITE_URL
1053
- 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)
1054
- const WeComApiContent = encodeURIComponent(WeComContent)
1055
- const WeComApiUrl = config.WECOM_API_URL
1056
- const sendResult = await axios.get(WeComApiUrl + WeComApiContent)
1057
- console.log('WinxinPush 通知结果:', sendResult)
1058
- }
1059
-
1060
- // 自定义钉钉WebHook通知
1061
- async function noticeDingTalkHook (comment) {
1062
- if (!config.DINGTALK_WEBHOOK_URL) {
1063
- console.log('没有配置 DingTalk_WebHook,放弃钉钉WebHook推送')
1064
- return
1065
- }
1066
- if (config.BLOGGER_EMAIL === comment.mail) return
1067
- 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)
1068
- const sendResult = await axios.post(config.DINGTALK_WEBHOOK_URL, { msgtype: 'text', text: { content: DingTalkContent } })
1069
- console.log('钉钉WebHook 通知结果:', sendResult)
1070
- }
1071
-
1072
- // QQ通知
1073
- async function noticeQQ (comment) {
1074
- if (!config.QM_SENDKEY) {
1075
- console.log('没有配置 qmsg 酱,放弃QQ通知')
1076
- return
1077
- }
1078
- if (config.BLOGGER_EMAIL === comment.mail) return
1079
- const pushContent = getIMPushContent(comment, { withUrl: false })
1080
- const qmApiUrl = 'https://qmsg.zendee.cn'
1081
- const qmApiParam = {
1082
- msg: pushContent.subject + '\n' + pushContent.content.replace(/<br>/g, '\n')
1083
- }
1084
- const sendResult = await axios.post(`${qmApiUrl}/send/${config.QM_SENDKEY}`, qs.stringify(qmApiParam), {
1085
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
1086
1004
  })
1087
- console.log('QQ通知结果:', sendResult)
1088
- }
1089
-
1090
- async function noticePushdeer (comment) {
1091
- if (!config.PUSHDEER_KEY) return
1092
- if (config.BLOGGER_EMAIL === comment.mail) return
1093
- const pushContent = getIMPushContent(comment, { markdown: true })
1094
- const sendResult = await axios.post('https://api2.pushdeer.com/message/push', {
1095
- pushkey: config.PUSHDEER_KEY,
1096
- text: pushContent.subject,
1097
- desp: pushContent.content
1098
- })
1099
- console.log('Pushdeer 通知结果:', sendResult)
1100
- }
1101
-
1102
- // QQ私有化API通知
1103
- async function noticeQQAPI (comment) {
1104
- if (!config.QQ_API) {
1105
- console.log('没有配置QQ私有化api,放弃QQ通知')
1106
- return
1107
- }
1108
- if (config.BLOGGER_EMAIL === comment.mail) return
1109
- const pushContent = getIMPushContent(comment)
1110
- const qqApiParam = {
1111
- message: pushContent.subject + '\n' + pushContent.content.replace(/<br>/g, '\n')
1112
- }
1113
- const sendResult = await axios.post(`${config.QQ_API}`, qs.stringify(qqApiParam))
1114
- console.log('QQ私有化api通知结果:', sendResult)
1005
+ console.log('即时消息通知结果:', sendResult)
1115
1006
  }
1116
1007
 
1117
1008
  // 即时消息推送内容获取
1118
- function getIMPushContent (comment, { withUrl = true, markdown = false, html = false } = {}) {
1009
+ function getIMPushContent (comment) {
1119
1010
  const SITE_NAME = config.SITE_NAME
1120
1011
  const NICK = comment.nick
1121
1012
  const MAIL = comment.mail
@@ -1124,17 +1015,13 @@ function getIMPushContent (comment, { withUrl = true, markdown = false, html = f
1124
1015
  const SITE_URL = config.SITE_URL
1125
1016
  const POST_URL = appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
1126
1017
  const subject = config.MAIL_SUBJECT_ADMIN || `${SITE_NAME}有新评论了`
1127
- let content = `评论人:${NICK}(${MAIL})<br>评论人IP:${IP}<br>评论内容:${COMMENT}<br>`
1128
- // Qmsg 会过滤带网址的推送消息,所以不能带网址
1129
- if (withUrl) {
1130
- content += `原文链接:${markdown ? `[${POST_URL}](${POST_URL})` : POST_URL}`
1131
- }
1132
- if (html) {
1133
- content += `原文链接:<a href="${POST_URL}" rel="nofollow">${POST_URL}</a>`
1134
- }
1135
- if (markdown) {
1136
- content = content.replace(/<br>/g, '\n\n')
1137
- }
1018
+ const content = `评论人:${NICK} ([${MAIL}](mailto:${MAIL}))
1019
+
1020
+ 评论人IP:${IP}
1021
+
1022
+ 评论内容:${COMMENT}
1023
+
1024
+ 原文链接:[${POST_URL}](${POST_URL})`
1138
1025
  return {
1139
1026
  subject,
1140
1027
  content
@@ -1251,7 +1138,8 @@ async function parse (comment) {
1251
1138
  // 限流
1252
1139
  async function limitFilter () {
1253
1140
  // 限制每个 IP 每 10 分钟发表的评论数量
1254
- const limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
1141
+ let limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
1142
+ if (Number.isNaN(limitPerMinute)) limitPerMinute = 10
1255
1143
  if (limitPerMinute) {
1256
1144
  const count = await db
1257
1145
  .collection('comment')
@@ -1264,7 +1152,8 @@ async function limitFilter () {
1264
1152
  }
1265
1153
  }
1266
1154
  // 限制所有 IP 每 10 分钟发表的评论数量
1267
- const limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
1155
+ let limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
1156
+ if (Number.isNaN(limitPerMinuteAll)) limitPerMinuteAll = 10
1268
1157
  if (limitPerMinuteAll) {
1269
1158
  const count = await db
1270
1159
  .collection('comment')
@@ -1518,6 +1407,41 @@ async function emailTest (event) {
1518
1407
  return res
1519
1408
  }
1520
1409
 
1410
+ async function uploadImage (event) {
1411
+ const { photo, fileName } = event
1412
+ const res = {}
1413
+ try {
1414
+ if (!config.IMAGE_CDN_TOKEN) {
1415
+ throw new Error('未配置图片上传服务')
1416
+ }
1417
+ const formData = new FormData()
1418
+ formData.append('image', base64UrlToReadStream(photo, fileName))
1419
+ const uploadResult = await axios.post('https://7bu.top/api/upload', formData, {
1420
+ headers: {
1421
+ ...formData.getHeaders(),
1422
+ token: config.IMAGE_CDN_TOKEN
1423
+ }
1424
+ })
1425
+ if (uploadResult.data.code === 200) {
1426
+ res.data = uploadResult.data.data
1427
+ } else {
1428
+ throw new Error(uploadResult.data.msg)
1429
+ }
1430
+ } catch (e) {
1431
+ console.error(e)
1432
+ res.code = RES_CODE.UPLOAD_FAILED
1433
+ res.err = e.message
1434
+ }
1435
+ return res
1436
+ }
1437
+
1438
+ function base64UrlToReadStream (base64Url, fileName) {
1439
+ const base64 = base64Url.split(';base64,').pop()
1440
+ const path = `/tmp/${fileName}`
1441
+ fs.writeFileSync(path, base64, { encoding: 'base64' })
1442
+ return fs.createReadStream(path)
1443
+ }
1444
+
1521
1445
  function getAvatar (comment) {
1522
1446
  if (comment.avatar) {
1523
1447
  return comment.avatar
@@ -1568,7 +1492,6 @@ async function getConfig () {
1568
1492
  DEFAULT_GRAVATAR: config.DEFAULT_GRAVATAR,
1569
1493
  SHOW_IMAGE: config.SHOW_IMAGE || 'true',
1570
1494
  IMAGE_CDN: config.IMAGE_CDN,
1571
- IMAGE_CDN_TOKEN: config.IMAGE_CDN_TOKEN,
1572
1495
  SHOW_EMOTION: config.SHOW_EMOTION || 'true',
1573
1496
  EMOTION_CDN: config.EMOTION_CDN,
1574
1497
  COMMENT_PLACEHOLDER: config.COMMENT_PLACEHOLDER,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twikoo-vercel",
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",
@@ -18,11 +18,12 @@
18
18
  "cheerio": "1.0.0-rc.5",
19
19
  "crypto-js": "^4.0.0",
20
20
  "dompurify": "^2.2.6",
21
+ "form-data": "^4.0.0",
21
22
  "jsdom": "^16.4.0",
22
23
  "marked": "^4.0.12",
23
24
  "mongodb": "^3.6.3",
24
25
  "nodemailer": "^6.4.17",
25
- "querystring": "^0.2.0",
26
+ "pushoo": "latest",
26
27
  "tencentcloud-sdk-nodejs": "^4.0.65",
27
28
  "uuid": "^8.3.2",
28
29
  "xml2js": "^0.4.23"