Skip to content

Commit

Permalink
feat(0.4.0): 功能更新 (#59)
Browse files Browse the repository at this point in the history
* 新增 评论框粘贴图片(可禁用)
* 新增 插入图片(可禁用)
* 新增 插入表情(可禁用)
* 新增 反垃圾可接入腾讯云内容安全
* 新增 可限制每个IP每10分钟最多发表多少条评论
* 新增 ctrl+enter 快捷回复 #57
* 新增 自定义评论框提示信息(placeholder)
* 新增 可配置只通过微信通知博主 #26
  • Loading branch information
imaegoo authored Dec 18, 2020
1 parent 02276c8 commit ab8cf71
Show file tree
Hide file tree
Showing 18 changed files with 571 additions and 71 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.css
*.md
*.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ yarn build # 编译 (dist/twikoo.all.min.js)

## 国际化 | I18N

鉴于腾讯云云开发在中国以外地区的支持程度(文档只有中文版本,环境没有国际版本,在中国以外地区访问缓慢),本项目暂无国际化计划,请各位站长知悉。<br>
鉴于腾讯云云开发在中国以外地区的支持程度(文档只有中文版本,环境没有国际版本,在中国以外地区访问缓慢),本项目暂无国际化计划。有国际化需求的站长,推荐使用另一个安全的评论系统 [Waline](https://waline.js.org/)。<br>
There are no plans to internationalize this project.

## 许可 | License
Expand Down
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports = {
'/quick-start',
'/faq',
'/api',
'/cms',
'/link'
],
lastUpdated: true,
Expand Down
2 changes: 1 addition & 1 deletion docs/.vuepress/theme/layouts/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<template #page-bottom>
<div class="page-edit">
<div id="twikoo"></div>
<script src="https://cdn.jsdelivr.net/npm/twikoo@0.3.3/dist/twikoo.all.min.js" ref="twikooJs"></script>
<script src="https://cdn.jsdelivr.net/npm/twikoo@0.4.0/dist/twikoo.all.min.js" ref="twikooJs"></script>
</div>
</template>
</ParentLayout>
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ yarn build # 编译 (dist/twikoo.all.min.js)

## 国际化 | I18N

鉴于腾讯云云开发在中国以外地区的支持程度(文档只有中文版本,环境没有国际版本,在中国以外地区访问缓慢),本项目暂无国际化计划,请各位站长知悉。<br>
鉴于腾讯云云开发在中国以外地区的支持程度(文档只有中文版本,环境没有国际版本,在中国以外地区访问缓慢),本项目暂无国际化计划。有国际化需求的,推荐使用另一个安全的评论系统 [Waline](https://waline.js.org/)。<br>
There are no plans to internationalize this project.

## 许可
Expand Down
25 changes: 25 additions & 0 deletions docs/cms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 反垃圾

## 使用腾讯云内容安全服务

Twikoo 支持接入腾讯云文本内容检测,使用深度学习技术,识别涉黄、涉政、涉恐等有害内容,同时支持用户配置词库,打击自定义的违规文本。

腾讯云文本内容检测是付费服务,提供 1 个月的免费试用,之后价格为 25 元/万条。如果您对反垃圾评论要求不高,也可以使用免费的 Akismet。

如何申请腾讯云文本内容检测

1. 访问[腾讯云控制台-文本内容安全](https://console.cloud.tencent.com/cms/text/overview),开通文本内容安全服务
2. 访问[腾讯云控制台-用户列表](https://console.cloud.tencent.com/cam),点击新建用户,点击快速创建
3. 输入用户名,访问方式选择“编程访问”,用户权限取消“AdministratorAccess”,只勾选“QcloudCMSFullAccess”
4. 点击“创建用户”
5. 复制“成功新建用户”页面的“SecretId”和“SecretKey”,到 Twikoo 管理面板“反垃圾”模块中配置
6. 测试反垃圾效果

成功后,站长可以在[腾讯云控制台-自定义库管理](https://console.cloud.tencent.com/cms/text/lib)配置自定义文本内容过滤。

## 使用 Akismet 反垃圾服务

Akismet (Automattic Kismet) 是应用广泛的一个垃圾留言过滤系统,其作者是大名鼎鼎的 WordPress 创始人 Matt Mullenweg,Akismet 也是 WordPress 默认安装的插件,其使用非常广泛,设计目标便是帮助博客网站来过滤垃圾留言。

1. 注册 [akismet.com](https://akismet.com)
2. 选择 Akismet Personal 订阅,复制得到的 Akismet API Key,到 Twikoo 管理面板“反垃圾”模块中配置
4 changes: 3 additions & 1 deletion docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,12 @@ Butterfly 目前支持 Twikoo,请查看 [Butterfly 安裝文檔(四) 主題配

``` html
<div id="tcomment"></div>
<script src="https://cdn.jsdelivr.net/npm/twikoo@0.3.3/dist/twikoo.all.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/twikoo@0.4.0/dist/twikoo.all.min.js"></script>
<script>twikoo.init({ envId: '您的环境id', el: '#tcomment' })</script>
```

> 建议使用 CDN 引入 Twikoo 的用户在链接地址上锁定版本,以免将来 Twikoo 升级时受到非兼容性更新的影响。
### 通过 NPM 引入

::: tip 提示
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "twikoo",
"version": "0.3.3",
"version": "0.4.0",
"description": "A simple comment system based on Tencent CloudBase (tcb).",
"author": "imaegoo <[email protected]> (https://github.com/imaegoo)",
"license": "MIT",
Expand Down Expand Up @@ -51,6 +51,7 @@
"blueimp-md5": "^2.18.0",
"element-ui": "^2.14.1",
"marked": "^1.2.5",
"owo": "^1.0.2",
"vue": "^2.6.12"
}
}
101 changes: 69 additions & 32 deletions src/function/twikoo/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*!
* Twikoo cloudbase function v0.3.3
* Twikoo cloudbase function v0.4.0
* (c) 2020-2020 iMaeGoo
* Released under the MIT License.
*/
Expand All @@ -17,6 +17,8 @@ const createDOMPurify = require('dompurify') // 反 XSS
const { JSDOM } = require('jsdom') // document.window 服务器版
const xml2js = require('xml2js') // XML 解析
const marked = require('marked') // Markdown 解析
const CryptoJS = require('crypto-js') // 编解码
const tencentcloud = require('tencentcloud-sdk-nodejs') // 腾讯云 API NODEJS SDK

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

// 常量 / constants
const VERSION = '0.3.3'
const VERSION = '0.4.0'
const RES_CODE = {
SUCCESS: 0,
FAIL: 1000,
Expand Down Expand Up @@ -746,6 +748,7 @@ async function initMailer () {
async function noticeMaster (comment) {
if (!transporter) if (!await initMailer()) return
if (config.BLOGGER_EMAIL === comment.mail) return
if (config.SC_MAIL_NOTIFY !== 'true' && config.SC_SENDKEY) return
const SITE_NAME = config.SITE_NAME
const NICK = comment.nick
const COMMENT = comment.comment
Expand Down Expand Up @@ -911,55 +914,85 @@ async function parse (comment) {

// 垃圾评论检测
async function checkSpam (comment) {
// 使用全局配置,不需要重新读取配置
if (!config.AKISMET_KEY) {
return false
} else if (config.AKISMET_KEY === 'MANUAL_REVIEW') {
// 限制每个 IP 每 10 分钟发表的评论数量
const limitPerMinute = parseInt(config.LIMIT_PER_MINUTE)
if (limitPerMinute) {
let count = await db
.collection('comment')
.where({
ip: comment.ip,
created: _.gt(Date.now() - 600000)
})
.count()
count = count.total
if (count > limitPerMinute) {
throw new Error('发言频率过高')
}
}
// 内容安全检测
if (config.AKISMET_KEY === 'MANUAL_REVIEW') {
console.log('已使用人工审核模式,评论审核后才会发表~')
return true
} else {
} else if ((config.QCLOUD_SECRET_ID && config.QCLOUD_SECRET_KEY) || config.AKISMET_KEY) {
// Akismet 服务响应慢,影响用户体验,通过调用自身,实现开启新的云函数进程,异步检测的效果
try {
await app.callFunction({
const isSpam = await app.callFunction({
name: 'twikoo',
data: {
event: 'CHECK_SPAM',
comment,
key: config.AKISMET_KEY,
blog: config.SITE_URL
comment
}
}, { timeout: 300 }) // 设置较短的 timeout 来实现异步
return isSpam.result.isSpam
} catch (e) {
console.log('开始异步检测垃圾评论')
}
return false // 暂时放行
}
return false
}

// 异步检测垃圾评论
async function checkSpamAction (event, context) {
if (!isRecursion(context)) return { code: RES_CODE.FORBIDDEN }
try {
let isSpam
const comment = event.comment
const akismetClient = new AkismetClient({
key: event.key,
blog: event.blog
})
const isValid = await akismetClient.verifyKey()
if (!isValid) {
console.log('Akismet key 不可用:', event.key)
return
if (config.QCLOUD_SECRET_ID && config.QCLOUD_SECRET_KEY) {
// 腾讯云内容安全
const client = new tencentcloud.tms.v20200713.Client({
credential: { secretId: config.QCLOUD_SECRET_ID, secretKey: config.QCLOUD_SECRET_KEY },
region: 'ap-shanghai',
profile: { httpProfile: { endpoint: 'tms.tencentcloudapi.com' } }
})
const checkResult = await client.TextModeration({
Content: CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(comment.comment)),
Device: { IP: comment.ip },
User: { Nickname: comment.nick }
})
console.log('腾讯云返回结果:', checkResult)
isSpam = checkResult.EvilFlag !== 0
} else if (config.AKISMET_KEY) {
// Akismet
const akismetClient = new AkismetClient({
key: event.key,
blog: event.blog
})
const isValid = await akismetClient.verifyKey()
if (!isValid) {
console.log('Akismet key 不可用:', event.key)
return
}
isSpam = await akismetClient.checkSpam({
user_ip: comment.ip,
user_agent: comment.ua,
permalink: comment.href,
comment_type: comment.rid ? 'reply' : 'comment',
comment_author: comment.nick,
comment_author_email: comment.mail,
comment_author_url: comment.link,
comment_content: comment.comment
})
}
const isSpam = await akismetClient.checkSpam({
user_ip: comment.ip,
user_agent: comment.ua,
permalink: comment.href,
comment_type: comment.rid ? 'reply' : 'comment',
comment_author: comment.nick,
comment_author_email: comment.mail,
comment_author_url: comment.link,
comment_content: comment.comment
})
console.log('垃圾评论检测结果:', isSpam)
if (isSpam) {
await db
Expand All @@ -972,7 +1005,7 @@ async function checkSpamAction (event, context) {
}
return { code: RES_CODE.SUCCESS, isSpam }
} catch (err) {
console.error('Akismet 异常:', err)
console.error('垃圾评论检测异常:', err)
return { code: RES_CODE.AKISMET_ERROR, message: err.message }
}
}
Expand Down Expand Up @@ -1158,7 +1191,11 @@ function getConfig () {
SITE_URL: config.SITE_URL,
MASTER_TAG: config.MASTER_TAG,
COMMENT_BG_IMG: config.COMMENT_BG_IMG,
GRAVATAR_CDN: config.GRAVATAR_CDN
GRAVATAR_CDN: config.GRAVATAR_CDN,
SHOW_IMAGE: config.SHOW_IMAGE || 'true',
SHOW_EMOTION: config.SHOW_EMOTION || 'true',
EMOTION_CDN: config.EMOTION_CDN,
COMMENT_PLACEHOLDER: config.COMMENT_PLACEHOLDER
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/function/twikoo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
"name": "twikoo-func",
"main": "index.js",
"dependencies": {
"@cloudbase/manager-node": "^3.8.2",
"@cloudbase/node-sdk": "^2.4.4",
"akismet-api": "^5.1.0",
"axios": "^0.21.0",
"blueimp-md5": "^2.18.0",
"bowser": "^2.11.0",
"cheerio": "^1.0.0-rc.3",
"crypto-js": "^4.0.0",
"dompurify": "^2.2.3",
"jsdom": "^16.4.0",
"marked": "^1.2.5",
"nodemailer": "^6.4.16",
"querystring": "^0.2.0",
"tencentcloud-sdk-nodejs": "^4.0.33",
"xml2js": "^0.4.23"
}
}
9 changes: 5 additions & 4 deletions src/view/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default {
.twikoo .el-textarea__inner {
color: currentColor;
background-color: transparent;
border-color: #90939930;
border-color: #90939950;
}
.twikoo .el-input__inner:hover,
.twikoo .el-textarea__inner:hover {
Expand All @@ -69,13 +69,14 @@ export default {
.twikoo .el-input-group__append,
.twikoo .el-input-group__prepend {
color: currentColor;
background-clip: padding-box;
background-color: #90939920;
border-color: #90939930;
border-color: #90939950;
}
.twikoo .el-button:not(.el-button--primary):not(.el-button--text) {
color: currentColor;
background-color: #90939920;
border-color: #90939930;
background-color: #90939910;
border-color: #90939950;
}
.twikoo .el-button:not(.el-button--primary):not(.el-button--text):active,
.twikoo .el-button:not(.el-button--primary):not(.el-button--text):focus,
Expand Down
10 changes: 10 additions & 0 deletions src/view/components/TkAdmin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@ export default {
backdrop-filter: blur(5px);
transition: all 0.5s ease;
}
.tk-admin::-webkit-scrollbar {
width: 5px;
background-color: transparent;
}
.tk-admin::-webkit-scrollbar-track {
background-color: transparent;
}
.tk-admin::-webkit-scrollbar-thumb {
background-color: #ffffff50;
}
.tk-admin.__show {
left: 0;
}
Expand Down
25 changes: 12 additions & 13 deletions src/view/components/TkAdminConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,27 @@ export default {
{ key: 'COMMENT_PAGE_SIZE', desc: '评论列表分页大小,默认为 8。', ph: '示例:8', value: '' },
{ key: 'MASTER_TAG', desc: '博主标识自定义文字,默认为 “博主”。', ph: '示例:站长', value: '' },
{ key: 'COMMENT_BG_IMG', desc: '评论框自定义背景图片 URL 地址。', ph: '', value: '' },
{ key: 'GRAVATAR_CDN', desc: '自定义头像 CDN 地址。如:cn.gravatar.com, sdn.geekzu.org, gravatar.loli.net', ph: '示例:sdn.geekzu.org', value: '' }
{ key: 'GRAVATAR_CDN', desc: '自定义头像 CDN 地址。如:cn.gravatar.com, sdn.geekzu.org, gravatar.loli.net', ph: '示例:sdn.geekzu.org', value: '' },
{ key: 'SHOW_IMAGE', desc: '启用插入图片功能,默认为:true', ph: '示例:false', value: '' },
{ key: 'SHOW_EMOTION', desc: '启用插入表情功能,默认为:true', ph: '示例:false', value: '' },
{ key: 'EMOTION_CDN', desc: '表情 CDN,默认为:https://cdn.jsdelivr.net/gh/imaegoo/emotion/owo.json', ph: '', value: '' },
{ key: 'COMMENT_PLACEHOLDER', desc: '评论框提示信息,可用<br>换行,默认为空', ph: '示例:', value: '' }
]
},
{
name: '反垃圾',
items: [
{ key: 'AKISMET_KEY', desc: 'Akismet 反垃圾评论,用于垃圾评论检测,设为 "MANUAL_REVIEW" 开启人工审核,留空不使用反垃圾。注册:https://akismet.com', ph: '示例:8651783edxxx', value: '' }
{ key: 'AKISMET_KEY', desc: 'Akismet 反垃圾评论,用于垃圾评论检测,设为 "MANUAL_REVIEW" 开启人工审核,留空不使用反垃圾。注册:https://akismet.com', ph: '示例:8651783edxxx', value: '' },
{ key: 'QCLOUD_SECRET_ID', desc: '腾讯云 secret id,用于垃圾评论检测。同时设置腾讯云和 Akismet 时,只有腾讯云会生效。注册:https://twikoo.js.org/cms.html', ph: '示例:AKIDBgZDdnbTw9D4ey9qPkrkwtb2Do9EwIHw', value: '' },
{ key: 'QCLOUD_SECRET_KEY', desc: '腾讯云 secret key', ph: '示例:XrkOnvKWS7WeXbP1QZT76rPgtpWx73D7', value: '' },
{ key: 'LIMIT_PER_MINUTE', desc: '每个 IP 每 10 分钟最多发表多少条评论,默认:0(无限制)', ph: '示例:5', value: '' }
]
},
{
name: '微信通知',
items: [
{ key: 'SC_SENDKEY', desc: 'Server酱(sc.ftqq.com)微信推送的 SCKEY', ph: '示例:SCT1364TKdsiGjGvyAZNYDVnuHW12345', value: '' }
{ key: 'SC_SENDKEY', desc: 'Server酱(sc.ftqq.com)微信推送的 SCKEY', ph: '示例:SCT1364TKdsiGjGvyAZNYDVnuHW12345', value: '' },
{ key: 'SC_MAIL_NOTIFY', desc: '是否同时通过微信和邮件 2 种方式通知博主,默认只通过微信通知博主,默认:false', ph: '示例:true', value: '' }
]
},
{
Expand Down Expand Up @@ -127,16 +135,6 @@ export default {
overflow-y: auto;
padding-right: 0.5em;
}
.tk-admin-config-groups::-webkit-scrollbar {
width: 5px;
background-color: transparent;
}
.tk-admin-config-groups::-webkit-scrollbar-track {
background-color: transparent;
}
.tk-admin-config-groups::-webkit-scrollbar-thumb {
background-color: #ffffff50;
}
.tk-admin-config-group-title {
margin-top: 1em;
font-size: 1.25rem;
Expand All @@ -157,6 +155,7 @@ export default {
}
.tk-admin-config-desc {
margin-top: 0.5em;
font-size: 0.75em;
}
.tk-admin-config-actions {
display: flex;
Expand Down
Loading

0 comments on commit ab8cf71

Please sign in to comment.