twikoo-vercel 1.4.16 → 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 -124
  2. package/package.json +3 -2
package/api/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Twikoo vercel function v1.4.16
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.16'
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,12 +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
+ noticePushoo(comment)
897
898
  ]).catch(console.error)
898
899
  return { code: RES_CODE.SUCCESS }
899
900
  }
@@ -938,16 +939,8 @@ async function initMailer ({ throwErr = false } = {}) {
938
939
  async function noticeMaster (comment) {
939
940
  if (!transporter) if (!await initMailer()) return
940
941
  if (config.BLOGGER_EMAIL === comment.mail) return
941
- const IM_PUSH_CONFIGS = [
942
- 'SC_SENDKEY',
943
- 'QM_SENDKEY',
944
- 'PUSH_PLUS_TOKEN',
945
- 'WECOM_API_URL',
946
- 'DINGTALK_WEBHOOK_URL',
947
- 'PUSHDEER_KEY'
948
- ]
949
942
  // 判断是否存在即时消息推送配置
950
- const hasIMPushConfig = IM_PUSH_CONFIGS.some(item => !!config[item])
943
+ const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
951
944
  // 存在即时消息推送配置,则默认不发送邮件给博主
952
945
  if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') return
953
946
  const SITE_NAME = config.SITE_NAME
@@ -996,110 +989,24 @@ async function noticeMaster (comment) {
996
989
  return sendResult
997
990
  }
998
991
 
999
- // 微信通知
1000
- async function noticeWeChat (comment) {
1001
- if (!config.SC_SENDKEY) {
1002
- console.log('没有配置 server 酱,放弃微信通知')
992
+ // 即时消息通知
993
+ async function noticePushoo (comment) {
994
+ if (!config.PUSHOO_CHANNEL || !config.PUSHOO_TOKEN) {
995
+ console.log('没有配置 pushoo,放弃即时消息通知')
1003
996
  return
1004
997
  }
1005
998
  if (config.BLOGGER_EMAIL === comment.mail) return
1006
999
  const pushContent = getIMPushContent(comment)
1007
- let scApiUrl = 'https://sc.ftqq.com'
1008
- let scApiParam = {
1009
- text: pushContent.subject,
1010
- desp: pushContent.content
1011
- }
1012
- if (config.SC_SENDKEY.substring(0, 3).toLowerCase() === 'sct') {
1013
- // 兼容 server 酱测试专版
1014
- scApiUrl = 'https://sctapi.ftqq.com'
1015
- scApiParam = {
1016
- title: pushContent.subject,
1017
- desp: pushContent.content
1018
- }
1019
- }
1020
- const sendResult = await axios.post(`${scApiUrl}/${config.SC_SENDKEY}.send`, qs.stringify(scApiParam), {
1021
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
1022
- })
1023
- console.log('微信通知结果:', sendResult)
1024
- }
1025
-
1026
- // pushplus 通知
1027
- async function noticePushPlus (comment) {
1028
- if (!config.PUSH_PLUS_TOKEN) {
1029
- console.log('没有配置 pushplus,放弃通知')
1030
- return
1031
- }
1032
- if (config.BLOGGER_EMAIL === comment.mail) return
1033
- const pushContent = getIMPushContent(comment)
1034
- const ppApiUrl = 'http://pushplus.hxtrip.com/send'
1035
- const ppApiParam = {
1036
- token: config.PUSH_PLUS_TOKEN,
1000
+ const sendResult = await pushoo(config.PUSHOO_CHANNEL, {
1001
+ token: config.PUSHOO_TOKEN,
1037
1002
  title: pushContent.subject,
1038
1003
  content: pushContent.content
1039
- }
1040
- const sendResult = await axios.post(ppApiUrl, ppApiParam)
1041
- console.log('pushplus 通知结果:', sendResult)
1042
- }
1043
-
1044
- // 自定义WeCom企业微信api通知
1045
- async function noticeWeComPush (comment) {
1046
- if (!config.WECOM_API_URL) {
1047
- console.log('未配置 WECOM_API_URL,跳过企业微信推送')
1048
- return
1049
- }
1050
- if (config.BLOGGER_EMAIL === comment.mail) return
1051
- const SITE_URL = config.SITE_URL
1052
- 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)
1053
- const WeComApiContent = encodeURIComponent(WeComContent)
1054
- const WeComApiUrl = config.WECOM_API_URL
1055
- const sendResult = await axios.get(WeComApiUrl + WeComApiContent)
1056
- console.log('WinxinPush 通知结果:', sendResult)
1057
- }
1058
-
1059
- // 自定义钉钉WebHook通知
1060
- async function noticeDingTalkHook (comment) {
1061
- if (!config.DINGTALK_WEBHOOK_URL) {
1062
- console.log('没有配置 DingTalk_WebHook,放弃钉钉WebHook推送')
1063
- return
1064
- }
1065
- if (config.BLOGGER_EMAIL === comment.mail) return
1066
- 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)
1067
- const sendResult = await axios.post(config.DINGTALK_WEBHOOK_URL, { msgtype: 'text', text: { content: DingTalkContent } })
1068
- console.log('钉钉WebHook 通知结果:', sendResult)
1069
- }
1070
-
1071
- // QQ通知
1072
- async function noticeQQ (comment) {
1073
- if (!config.QM_SENDKEY) {
1074
- console.log('没有配置 qmsg 酱,放弃QQ通知')
1075
- return
1076
- }
1077
- if (config.BLOGGER_EMAIL === comment.mail) return
1078
- const pushContent = getIMPushContent(comment, { withUrl: false })
1079
- const qmApiUrl = 'https://qmsg.zendee.cn'
1080
- const qmApiParam = {
1081
- msg: pushContent.subject + '\n' + pushContent.content.replace(/<br>/g, '\n')
1082
- }
1083
- const sendResult = await axios.post(`${qmApiUrl}/send/${config.QM_SENDKEY}`, qs.stringify(qmApiParam), {
1084
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
1085
1004
  })
1086
- console.log('QQ通知结果:', sendResult)
1087
- }
1088
-
1089
- async function noticePushdeer (comment) {
1090
- if (!config.PUSHDEER_KEY) return
1091
- if (config.BLOGGER_EMAIL === comment.mail) return
1092
- const pushContent = getIMPushContent(comment, { markdown: true })
1093
- const sendResult = await axios.post('https://api2.pushdeer.com/message/push', {
1094
- pushkey: config.PUSHDEER_KEY,
1095
- text: pushContent.subject,
1096
- desp: pushContent.content
1097
- })
1098
- console.log('Pushdeer 通知结果:', sendResult)
1005
+ console.log('即时消息通知结果:', sendResult)
1099
1006
  }
1100
1007
 
1101
1008
  // 即时消息推送内容获取
1102
- function getIMPushContent (comment, { withUrl = true, markdown = false }) {
1009
+ function getIMPushContent (comment) {
1103
1010
  const SITE_NAME = config.SITE_NAME
1104
1011
  const NICK = comment.nick
1105
1012
  const MAIL = comment.mail
@@ -1108,14 +1015,13 @@ function getIMPushContent (comment, { withUrl = true, markdown = false }) {
1108
1015
  const SITE_URL = config.SITE_URL
1109
1016
  const POST_URL = appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
1110
1017
  const subject = config.MAIL_SUBJECT_ADMIN || `${SITE_NAME}有新评论了`
1111
- let content = `评论人:${NICK}(${MAIL})<br>评论人IP:${IP}<br>评论内容:${COMMENT}`
1112
- // Qmsg 会过滤带网址的推送消息,所以不能带网址
1113
- if (withUrl) {
1114
- content += `<br>您可以点击 ${markdown ? `[${POST_URL}](${POST_URL})` : POST_URL} 查看回复的完整內容`
1115
- }
1116
- if (markdown) {
1117
- content = content.replace(/<br>/g, '\n\n')
1118
- }
1018
+ const content = `评论人:${NICK} ([${MAIL}](mailto:${MAIL}))
1019
+
1020
+ 评论人IP:${IP}
1021
+
1022
+ 评论内容:${COMMENT}
1023
+
1024
+ 原文链接:[${POST_URL}](${POST_URL})`
1119
1025
  return {
1120
1026
  subject,
1121
1027
  content
@@ -1232,7 +1138,8 @@ async function parse (comment) {
1232
1138
  // 限流
1233
1139
  async function limitFilter () {
1234
1140
  // 限制每个 IP 每 10 分钟发表的评论数量
1235
- const limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
1141
+ let limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
1142
+ if (Number.isNaN(limitPerMinute)) limitPerMinute = 10
1236
1143
  if (limitPerMinute) {
1237
1144
  const count = await db
1238
1145
  .collection('comment')
@@ -1245,7 +1152,8 @@ async function limitFilter () {
1245
1152
  }
1246
1153
  }
1247
1154
  // 限制所有 IP 每 10 分钟发表的评论数量
1248
- const limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
1155
+ let limitPerMinuteAll = parseInt(config.LIMIT_PER_MINUTE_ALL)
1156
+ if (Number.isNaN(limitPerMinuteAll)) limitPerMinuteAll = 10
1249
1157
  if (limitPerMinuteAll) {
1250
1158
  const count = await db
1251
1159
  .collection('comment')
@@ -1499,6 +1407,41 @@ async function emailTest (event) {
1499
1407
  return res
1500
1408
  }
1501
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
+
1502
1445
  function getAvatar (comment) {
1503
1446
  if (comment.avatar) {
1504
1447
  return comment.avatar
@@ -1549,7 +1492,6 @@ async function getConfig () {
1549
1492
  DEFAULT_GRAVATAR: config.DEFAULT_GRAVATAR,
1550
1493
  SHOW_IMAGE: config.SHOW_IMAGE || 'true',
1551
1494
  IMAGE_CDN: config.IMAGE_CDN,
1552
- IMAGE_CDN_TOKEN: config.IMAGE_CDN_TOKEN,
1553
1495
  SHOW_EMOTION: config.SHOW_EMOTION || 'true',
1554
1496
  EMOTION_CDN: config.EMOTION_CDN,
1555
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.16",
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"