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.
- package/index.js +100 -147
- package/package.json +3 -2
package/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Twikoo cloudbase function
|
|
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
|
-
|
|
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('
|
|
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 =
|
|
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
|
|
991
|
-
if (!config.
|
|
992
|
-
console.log('没有配置
|
|
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
|
-
|
|
998
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
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('
|
|
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
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
27
|
+
"pushoo": "latest",
|
|
27
28
|
"tencentcloud-sdk-nodejs": "^4.0.65",
|
|
28
29
|
"xml2js": "^0.4.23"
|
|
29
30
|
}
|