Skip to content

Commit a21863c

Browse files
authored
feat: support pushoo | fix: upload image via serverless to protect 7bu token (#329)
* fix: upload image via serverless to protect 7bu token * feat: support pushoo
1 parent af5312b commit a21863c

File tree

13 files changed

+260
-389
lines changed

13 files changed

+260
-389
lines changed

docs/.vuepress/theme/layouts/Layout.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
<!-- Twikoo -->
1515
<div id="twikoo"></div>
16-
<script src="https://cdn.jsdelivr.net/npm/twikoo@1.4.18/dist/twikoo.all.min.js" ref="twikooJs"></script>
16+
<script src="https://cdn.jsdelivr.net/npm/twikoo@1.5.0/dist/twikoo.all.min.js" ref="twikooJs"></script>
1717
</div>
1818
</template>
1919
</ParentLayout>

docs/quick-start.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ exports.main = require('twikoo-func').main
4444
8. 创建完成后,点击“twikoo"进入云函数详情页,进入“函数代码”标签,点击“文件 - 新建文件”,输入 `package.json`,回车
4545
9. 复制以下代码、粘贴到代码框中,点击“保存并安装依赖”
4646
``` json
47-
{ "dependencies": { "twikoo-func": "1.4.18" } }
47+
{ "dependencies": { "twikoo-func": "1.5.0" } }
4848
```
4949

5050
### 命令行部署
@@ -174,7 +174,7 @@ twikoo:
174174

175175
``` html
176176
<div id="tcomment"></div>
177-
<script src="https://cdn.jsdelivr.net/npm/twikoo@1.4.18/dist/twikoo.all.min.js"></script>
177+
<script src="https://cdn.jsdelivr.net/npm/twikoo@1.5.0/dist/twikoo.all.min.js"></script>
178178
<script>
179179
twikoo.init({
180180
envId: '您的环境id',
@@ -195,8 +195,8 @@ twikoo.init({
195195
引入的 CDN 链接替换为如下即可:
196196

197197
```diff
198-
- <script src="https://cdn.jsdelivr.net/npm/twikoo@1.4.18/dist/twikoo.all.min.js"></script>
199-
+ <script src="https://lib.baomitu.com/twikoo/1.4.18/twikoo.all.min.js" crossorigin="anonymous" integrity="sha512-czTF7AsBQKM8Udh7f2kYxoEVO6MRUGoBACWgrnURTySkkV+wBwzOiFncA2fjR2JSOJ6vaTGILYIE1laKPH8fKA=="></script>
198+
- <script src="https://cdn.jsdelivr.net/npm/twikoo@1.5.0/dist/twikoo.all.min.js"></script>
199+
+ <script src="https://lib.baomitu.com/twikoo/1.5.0/twikoo.all.min.js" crossorigin="anonymous" integrity="sha512-czTF7AsBQKM8Udh7f2kYxoEVO6MRUGoBACWgrnURTySkkV+wBwzOiFncA2fjR2JSOJ6vaTGILYIE1laKPH8fKA=="></script>
200200
```
201201

202202
## 开启管理面板

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"name": "twikoo",
3-
"version": "1.4.18",
3+
"version": "1.5.0",
44
"description": "A simple comment system based on Tencent CloudBase (tcb).",
5+
"keywords": ["twikoojs", "comment", "comment-system", "cloudbase", "vercel"],
56
"author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
67
"license": "MIT",
78
"main": "./dist/twikoo.all.min.js",

src/function/twikoo/index.js

Lines changed: 67 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*!
2-
* Twikoo cloudbase function v1.4.18
2+
* Twikoo cloudbase function v1.5.0
33
* (c) 2020-present iMaeGoo
44
* Released under the MIT License.
55
*/
@@ -10,7 +10,6 @@ const md5 = require('blueimp-md5') // MD5 加解密
1010
const bowser = require('bowser') // UserAgent 格式化
1111
const nodemailer = require('nodemailer') // 发送邮件
1212
const axios = require('axios') // 发送 REST 请求
13-
const qs = require('querystring') // URL 参数格式化
1413
const $ = require('cheerio') // jQuery 服务器版
1514
const { AkismetClient } = require('akismet-api') // 反垃圾 API
1615
const createDOMPurify = require('dompurify') // 反 XSS
@@ -19,6 +18,9 @@ const xml2js = require('xml2js') // XML 解析
1918
const marked = require('marked') // Markdown 解析
2019
const CryptoJS = require('crypto-js') // 编解码
2120
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
2224

2325
// 云函数 SDK / tencent cloudbase sdk
2426
const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })
@@ -31,7 +33,7 @@ const window = new JSDOM('').window
3133
const DOMPurify = createDOMPurify(window)
3234

3335
// 常量 / constants
34-
const VERSION = '1.4.18'
36+
const VERSION = '1.5.0'
3537
const RES_CODE = {
3638
SUCCESS: 0,
3739
FAIL: 1000,
@@ -44,7 +46,8 @@ const RES_CODE = {
4446
PASS_NOT_MATCH: 1023,
4547
NEED_LOGIN: 1024,
4648
FORBIDDEN: 1403,
47-
AKISMET_ERROR: 1030
49+
AKISMET_ERROR: 1030,
50+
UPLOAD_FAILED: 1040
4851
}
4952
const ADMIN_USER_ID = 'admin'
5053

@@ -119,6 +122,9 @@ exports.main = async (event, context) => {
119122
case 'EMAIL_TEST': // >= 1.4.6
120123
res = await emailTest(event)
121124
break
125+
case 'UPLOAD_IMAGE': // >= 1.5.0
126+
res = await uploadImage(event)
127+
break
122128
default:
123129
if (event.event) {
124130
res.code = RES_CODE.EVENT_NOT_EXIST
@@ -876,15 +882,9 @@ async function sendNotice (comment) {
876882
await Promise.all([
877883
noticeMaster(comment),
878884
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)
886886
]).catch(err => {
887-
console.error('邮件通知异常:', err)
887+
console.error('通知异常:', err)
888888
})
889889
}
890890

@@ -928,16 +928,8 @@ async function initMailer ({ throwErr = false } = {}) {
928928
async function noticeMaster (comment) {
929929
if (!transporter) if (!await initMailer()) return
930930
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-
]
939931
// 判断是否存在即时消息推送配置
940-
const hasIMPushConfig = IM_PUSH_CONFIGS.some(item => !!config[item])
932+
const hasIMPushConfig = config.PUSHOO_CHANNEL && config.PUSHOO_TOKEN
941933
// 存在即时消息推送配置,则默认不发送邮件给博主
942934
if (hasIMPushConfig && config.SC_MAIL_NOTIFY !== 'true') return
943935
const SITE_NAME = config.SITE_NAME
@@ -986,125 +978,24 @@ async function noticeMaster (comment) {
986978
return sendResult
987979
}
988980

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,放弃即时消息通知')
993985
return
994986
}
995987
if (config.BLOGGER_EMAIL === comment.mail) return
996988
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,
1027991
title: pushContent.subject,
1028992
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
1087993
})
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)
1104995
}
1105996

1106997
// 即时消息推送内容获取
1107-
function getIMPushContent (comment, { withUrl = true, markdown = false, html = false } = {}) {
998+
function getIMPushContent (comment) {
1108999
const SITE_NAME = config.SITE_NAME
11091000
const NICK = comment.nick
11101001
const MAIL = comment.mail
@@ -1113,17 +1004,13 @@ function getIMPushContent (comment, { withUrl = true, markdown = false, html = f
11131004
const SITE_URL = config.SITE_URL
11141005
const POST_URL = appendHashToUrl(comment.href || SITE_URL + comment.url, comment.id)
11151006
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})`
11271014
return {
11281015
subject,
11291016
content
@@ -1241,7 +1128,8 @@ async function parse (comment) {
12411128
// 限流
12421129
async function limitFilter () {
12431130
// 限制每个 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
12451133
if (limitPerMinute) {
12461134
let count = await db
12471135
.collection('comment')
@@ -1256,7 +1144,8 @@ async function limitFilter () {
12561144
}
12571145
}
12581146
// 限制所有 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
12601149
if (limitPerMinuteAll) {
12611150
let count = await db
12621151
.collection('comment')
@@ -1511,6 +1400,41 @@ async function emailTest (event) {
15111400
return res
15121401
}
15131402

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+
15141438
function getAvatar (comment) {
15151439
if (comment.avatar) {
15161440
return comment.avatar

src/function/twikoo/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "twikoo-func",
3-
"version": "1.4.18",
3+
"version": "1.5.0",
44
"description": "A simple comment system based on Tencent CloudBase (tcb).",
55
"author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
66
"license": "MIT",
@@ -20,10 +20,11 @@
2020
"cheerio": "1.0.0-rc.5",
2121
"crypto-js": "^4.0.0",
2222
"dompurify": "^2.2.6",
23+
"form-data": "^4.0.0",
2324
"jsdom": "^16.4.0",
2425
"marked": "^4.0.12",
2526
"nodemailer": "^6.4.17",
26-
"querystring": "^0.2.0",
27+
"pushoo": "latest",
2728
"tencentcloud-sdk-nodejs": "^4.0.65",
2829
"xml2js": "^0.4.23"
2930
}

src/js/entites/config.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,8 @@ class Config {
3333
this.SMTP_SECURE = model.SMTP_SECURE
3434
this.SMTP_USER = model.SMTP_USER
3535
this.SMTP_PASS = model.SMTP_PASS
36-
this.SC_SENDKEY = model.SC_SENDKEY
37-
this.QM_SENDKEY = model.QM_SENDKEY
38-
this.QQ_API = model.QQ_API
39-
this.PUSH_PLUS_TOKEN = model.PUSH_PLUS_TOKEN
40-
this.WECOM_API_URL = model.WECOM_API_URL
41-
this.DINGTALK_WEBHOOK_URL = model.DINGTALK_WEBHOOK_URL
42-
this.PUSHDEER_KEY = model.PUSHDEER_KEY
36+
this.PUSHOO_CHANNEL = model.PUSHOO_CHANNEL
37+
this.PUSHOO_TOKEN = model.PUSHOO_TOKEN
4338
this.SENDER_NAME = model.SENDER_NAME
4439
this.SENDER_EMAIL = model.SENDER_EMAIL
4540
this.BLOGGER_EMAIL = model.BLOGGER_EMAIL

0 commit comments

Comments
 (0)