Skip to content

Commit 2c7310c

Browse files
authored
fix(0.2.5): 新增评论审核,修复问题 (#35)
* 新增 AKISMET 评论审核 * 新增 人工审核 * 修复 配置表为空时,无法保存配置 * 完善文档
1 parent 0e10f7a commit 2c7310c

File tree

12 files changed

+152
-56
lines changed

12 files changed

+152
-56
lines changed

README.md

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,26 @@ A simple, safe, serverless comment system based on Tencent CloudBase (tcb).
1010

1111
## 特色 | Features
1212

13-
* 评论 | Comment
14-
* 点赞 | Like
15-
* 纯静态 | Static pages
16-
* 可嵌入 | Embedded
17-
* 免费搭建 | Free deploy
18-
* 存储安全 | Security
19-
* 反垃圾评论 | Akismet
20-
* 邮件 / 微信通知 | Email / WeChat notify
21-
22-
## 演示 | Demo
23-
24-
请查看[twikoo.js.org](https://twikoo.js.org)
13+
* 免费搭建(※1)
14+
* 隐私安全(※2)
15+
* 支持邮件与微信通知(※3)
16+
* 支持反垃圾评论(※4)
17+
18+
注:<br>
19+
※1 Twikoo 使用云开发作为评论后台,每个用户均长期享受1个免费的标准型基础版1资源套餐<br>
20+
※2 Twikoo 通过云函数控制敏感字段(邮箱、IP、环境配置等)不会泄露<br>
21+
※3 微信提醒基于 [Server酱](https://sc.ftqq.com/3.version),需自行注册并获取API key<br>
22+
※4 反垃圾基于 [akismet.com](https://akismet.com/),需自行注册并获取API key
23+
24+
## 预览 | Preview
25+
26+
### 评论
27+
28+
![评论](./docs/static/readme-1.png)
29+
30+
### 评论管理
31+
32+
![评论管理](./docs/static/readme-2.png)
2533

2634
## 快速上手 | Quick Start
2735

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<template #page-bottom>
44
<div class="page-edit">
55
<div id="twikoo"></div>
6-
<script src="https://cdn.jsdelivr.net/npm/twikoo@0.2.4/dist/twikoo.all.min.js" ref="twikooJs"></script>
6+
<script src="https://cdn.jsdelivr.net/npm/twikoo@0.2.5/dist/twikoo.all.min.js" ref="twikooJs"></script>
77
</div>
88
</template>
99
</ParentLayout>

docs/README.md

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,26 @@ A simple, safe, serverless comment system based on Tencent CloudBase (tcb).
1010

1111
## 特色
1212

13-
* 评论 | Comment
14-
* 点赞 | Like
15-
* 纯静态 | Static pages
16-
* 可嵌入 | Embedded
17-
* 免费搭建 | Free deploy
18-
* 存储安全 | Security
19-
* 反垃圾评论 | Akismet
20-
* 邮件 / 微信通知 | Email / WeChat notify
13+
* 免费搭建(※1)
14+
* 隐私安全(※2)
15+
* 支持邮件与微信通知(※3)
16+
* 支持反垃圾评论(※4)
17+
18+
> 注:<br>
19+
> ※1 Twikoo 使用云开发作为评论后台,每个用户均长期享受1个免费的标准型基础版1资源套餐<br>
20+
> ※2 Twikoo 通过云函数控制敏感字段(邮箱、IP、环境配置等)不会泄露<br>
21+
> ※3 微信提醒基于 [Server酱](https://sc.ftqq.com/3.version),需自行注册并获取API key<br>
22+
> ※4 反垃圾基于 [akismet.com](https://akismet.com/),需自行注册并获取API key
2123
2224
## 预览
2325

24-
[https://www.imaegoo.com/2020/hello-twikoo/](https://www.imaegoo.com/2020/hello-twikoo/)
26+
### 评论
27+
28+
![评论](./static/readme-1.png)
29+
30+
### 评论管理
31+
32+
![评论管理](./static/readme-2.png)
2533

2634
## 交流群
2735

@@ -47,8 +55,8 @@ A simple, safe, serverless comment system based on Tencent CloudBase (tcb).
4755
| ---- | ---- | ---- |
4856
| 已完成 | P0 | 文档撰写 |
4957
| 已完成 | P1 | 评论管理 |
50-
| 开发中 | P2 | 人工审核 |
51-
| 开发中 | P2 | AKISMET 审核 |
58+
| 已完成 | P2 | 人工审核 |
59+
| 已完成 | P2 | AKISMET 审核 |
5260
| 开发中 | P2 | 完整 Markdown 适配 |
5361
| 计划中 | P3 | Emoji 表情 |
5462
| 计划中 | P3 | 图片表情 |

docs/quick-start.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ Butterfly 目前支持 Twikoo,请查看 [Butterfly 安裝文檔(四) 主題配
9090

9191
``` html
9292
<div id="tcomment"></div>
93-
<script src="https://cdn.jsdelivr.net/npm/twikoo@0.2.4/dist/twikoo.all.min.js"></script>
93+
<script src="https://cdn.jsdelivr.net/npm/twikoo@0.2.5/dist/twikoo.all.min.js"></script>
9494
<script>twikoo.init({ envId: '您的环境id', el: '#tcomment' })</script>
9595
```
9696

docs/static/readme-1.png

8.93 KB
Loading

docs/static/readme-2.png

15.3 KB
Loading

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "twikoo",
3-
"version": "0.2.4",
3+
"version": "0.2.5",
44
"description": "A simple comment system based on Tencent CloudBase (tcb).",
55
"author": "imaegoo <hello@imaegoo.com> (https://github.com/imaegoo)",
66
"license": "MIT",

src/function/twikoo/index.js

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*!
2-
* Twikoo cloudbase function v0.2.4
2+
* Twikoo cloudbase function v0.2.5
33
* (c) 2020-2020 iMaeGoo
44
* Released under the MIT License.
55
*/
@@ -12,6 +12,7 @@ const nodemailer = require('nodemailer')
1212
const axios = require('axios')
1313
const qs = require('querystring')
1414
const $ = require('cheerio')
15+
const { AkismetClient } = require('akismet-api')
1516

1617
// 云函数 SDK / tencent cloudbase sdk
1718
const app = tcb.init({ env: tcb.SYMBOL_CURRENT_ENV })
@@ -20,7 +21,7 @@ const db = app.database()
2021
const _ = db.command
2122

2223
// 常量 / constants
23-
const VERSION = '0.2.4'
24+
const VERSION = '0.2.5'
2425
const RES_CODE = {
2526
SUCCESS: 0,
2627
FAIL: 1000,
@@ -58,6 +59,9 @@ exports.main = async (event, context) => {
5859
case 'COMMENT_GET_FOR_ADMIN':
5960
res = await commentGetForAdmin(event)
6061
break
62+
case 'COMMENT_SET_FOR_ADMIN':
63+
res = await commentSetForAdmin(event)
64+
break
6165
case 'COMMENT_DELETE_FOR_ADMIN':
6266
res = await commentDeleteForAdmin(event)
6367
break
@@ -77,10 +81,10 @@ exports.main = async (event, context) => {
7781
res = await setPassword(event)
7882
break
7983
case 'GET_CONFIG':
80-
res = await getConfig(event)
84+
res = await getConfig()
8185
break
8286
case 'GET_CONFIG_FOR_ADMIN':
83-
res = await getConfigForAdmin(event)
87+
res = await getConfigForAdmin()
8488
break
8589
case 'SET_CONFIG':
8690
res = await setConfig(event)
@@ -297,6 +301,25 @@ function parseCommentForAdmin (comments) {
297301
return comments
298302
}
299303

304+
// 管理员修改评论
305+
async function commentSetForAdmin (event) {
306+
const res = {}
307+
const isAdminUser = await isAdmin()
308+
if (isAdminUser) {
309+
validate(event, ['id', 'set'])
310+
const data = await db
311+
.collection('comment')
312+
.doc(event.id)
313+
.update(event.set)
314+
res.code = RES_CODE.SUCCESS
315+
res.updated = data.updated
316+
} else {
317+
res.code = RES_CODE.NEED_LOGIN
318+
res.message = '请先登录'
319+
}
320+
return res
321+
}
322+
300323
// 管理员删除评论
301324
async function commentDeleteForAdmin (event) {
302325
const res = {}
@@ -374,21 +397,15 @@ async function commentSubmit (event) {
374397
await createCollections()
375398
await readConfig()
376399
}
377-
let comment
378-
try {
379-
comment = await save(event)
380-
} catch (e) {
381-
await createCollections()
382-
comment = await save(event)
383-
}
400+
const comment = await save(event)
384401
res.id = comment.id
385402
await sendMail(comment)
386403
return res
387404
}
388405

389406
// 保存评论
390407
async function save (event) {
391-
const data = parse(event)
408+
const data = await parse(event)
392409
const result = await db
393410
.collection('comment')
394411
.add(data)
@@ -545,9 +562,9 @@ async function noticeReply (currentComment) {
545562
}
546563

547564
// 将评论转为数据库存储格式
548-
function parse (comment) {
565+
async function parse (comment) {
549566
const timestamp = new Date().getTime()
550-
return {
567+
const commentDo = {
551568
nick: comment.nick ? comment.nick : 'Anonymous',
552569
mail: comment.mail ? comment.mail : '',
553570
mailMd5: comment.mail ? md5(comment.mail) : '',
@@ -563,6 +580,45 @@ function parse (comment) {
563580
created: timestamp,
564581
updated: timestamp
565582
}
583+
commentDo.isSpam = await checkSpam(commentDo)
584+
return commentDo
585+
}
586+
587+
// 垃圾评论检测
588+
async function checkSpam (comment) {
589+
// 使用全局配置,不需要重新读取配置
590+
if (!config.AKISMET_KEY) {
591+
return false
592+
} else if (config.AKISMET_KEY === 'MANUAL_REVIEW') {
593+
console.log('已使用人工审核模式,评论审核后才会发表~')
594+
return true
595+
} else {
596+
try {
597+
const akismetClient = new AkismetClient({
598+
key: config.AKISMET_KEY,
599+
blog: config.SITE_URL
600+
})
601+
const isValid = await akismetClient.verifyKey()
602+
if (!isValid) {
603+
console.log('Akismet key 不可用:', config.AKISMET_KEY)
604+
return
605+
}
606+
const isSpam = await akismetClient.checkSpam({
607+
user_ip: comment.ip,
608+
user_agent: comment.ua,
609+
permalink: comment.href,
610+
comment_type: comment.rid ? 'reply' : 'comment',
611+
comment_author: comment.nick,
612+
comment_author_email: comment.mail,
613+
comment_author_url: comment.link,
614+
comment_content: comment.comment
615+
})
616+
console.log('垃圾评论检测结果:', isSpam)
617+
return isSpam
618+
} catch (err) {
619+
console.error('Akismet 异常:', err)
620+
}
621+
}
566622
}
567623

568624
/**
@@ -692,16 +748,14 @@ async function writeConfig (newConfig) {
692748
console.log('写入配置:', newConfig)
693749
try {
694750
let updated
695-
const existConfig = await readConfig()
696-
if (existConfig) {
697-
const res = await db
698-
.collection('config')
699-
.where({}) // 不加 where 会报错 Error: param should have required property 'query'
700-
.limit(1)
701-
.update(newConfig)
702-
updated = res.updated
703-
} else {
704-
const res = await db
751+
let res = await db
752+
.collection('config')
753+
.where({}) // 不加 where 会报错 Error: param should have required property 'query'
754+
.limit(1)
755+
.update(newConfig)
756+
updated = res.updated
757+
if (updated === 0) {
758+
res = await db
705759
.collection('config')
706760
.add(newConfig)
707761
updated = res.id ? 1 : 0

src/function/twikoo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"main": "index.js",
44
"dependencies": {
55
"@cloudbase/node-sdk": "^2.4.0",
6+
"akismet-api": "^5.1.0",
67
"axios": "^0.21.0",
78
"blueimp-md5": "^2.18.0",
89
"bowser": "^2.11.0",

src/view/components/TkAdminComment.vue

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,18 @@
55
<el-table-column prop="mail" label="邮箱" show-overflow-tooltip />
66
<el-table-column prop="link" label="网址" show-overflow-tooltip />
77
<el-table-column prop="commentText" label="评论" show-overflow-tooltip />
8-
<el-table-column width="150" label="操作">
8+
<el-table-column prop="isSpam" width="50" :formatter="statusFormatter" label="状态" />
9+
<el-table-column width="50" label="操作">
910
<template slot-scope="scope">
10-
<el-button size="mini" @click="handleView(scope.row)" type="primary">查看</el-button>
11-
<el-button size="mini" @click="handleDelete(scope.row)" type="danger">删除</el-button>
11+
<el-tooltip placement="left" width="160">
12+
<el-button type="text">操作</el-button>
13+
<div class="tk-admin-actions" slot="content">
14+
<el-button size="mini" @click="handleView(scope.row)" type="primary">查看</el-button>
15+
<el-button size="mini" v-if="scope.row.isSpam" @click="handleSpam(scope.row, false)" type="warning">显示</el-button>
16+
<el-button size="mini" v-if="!scope.row.isSpam" @click="handleSpam(scope.row, true)" type="warning">隐藏</el-button>
17+
<el-button size="mini" @click="handleDelete(scope.row)" type="danger">删除</el-button>
18+
</div>
19+
</el-tooltip>
1220
</template>
1321
</el-table-column>
1422
</el-table>
@@ -53,6 +61,9 @@ export default {
5361
this.currentPage = e
5462
this.getComments()
5563
},
64+
statusFormatter (row, column, cell) {
65+
return cell ? '隐藏' : '显示'
66+
},
5667
handleView (comment) {
5768
window.open(comment.href || comment.url)
5869
},
@@ -63,6 +74,15 @@ export default {
6374
})
6475
await this.getComments()
6576
this.loading = false
77+
},
78+
async handleSpam (comment, isSpam) {
79+
this.loading = true
80+
await call(this.$tcb, 'COMMENT_SET_FOR_ADMIN', {
81+
id: comment._id,
82+
set: { isSpam }
83+
})
84+
await this.getComments()
85+
this.loading = false
6686
}
6787
},
6888
mounted () {
@@ -122,4 +142,7 @@ export default {
122142
.tk-admin-comment /deep/ .el-icon-d-arrow-right::before {
123143
content: '...';
124144
}
145+
.tk-admin-actions {
146+
display: flex;
147+
}
125148
</style>

src/view/components/TkAdminConfig.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export default {
3939
{
4040
name: '反垃圾',
4141
items: [
42-
{ key: 'AKISMET_KEY', desc: '反垃圾评论 API key(暂未上线,敬请期待)。', ph: '示例:8651783ed123', value: '' }
42+
{ key: 'AKISMET_KEY', desc: 'Akismet 反垃圾评论,用于垃圾评论检测,设为 "MANUAL_REVIEW" 开启人工审核,留空不使用反垃圾。注册:https://akismet.com', ph: '示例:8651783edxxx', value: '' }
4343
]
4444
},
4545
{
@@ -53,7 +53,7 @@ export default {
5353
items: [
5454
{ key: 'SENDER_EMAIL', desc: '邮件通知邮箱地址。对于大多数邮箱服务商,SENDER_EMAIL 必须和 SMTP_USER 保持一致,否则无法发送邮件。', ph: '示例:blog@imaegoo.com', value: '' },
5555
{ key: 'SENDER_NAME', desc: '邮件通知标题。', ph: '示例:虹墨空间站评论提醒', value: '' },
56-
{ key: 'SMTP_SERVICE', desc: '邮件通知邮箱服务商。https://nodemailer.com/smtp/well-known/#supported-services', ph: '示例:QQ', value: '' },
56+
{ key: 'SMTP_SERVICE', desc: '邮件通知邮箱服务商。支持:"126", "163", "1und1", "AOL", "DebugMail", "DynectEmail", "FastMail", "GandiMail", "Gmail", "Godaddy", "GodaddyAsia", "GodaddyEurope", "Hotmail", "Mail.ru", "Maildev", "Mailgun", "Mailjet", "Mailosaur", "Mandrill", "Naver", "OpenMailBox", "Outlook365", "Postmark", "QQ", "QQex", "SES", "SES-EU-WEST-1", "SES-US-EAST-1", "SES-US-WEST-2", "SendCloud", "SendGrid", "SendPulse", "SendinBlue", "Sparkpost", "Yahoo", "Yandex", "Zoho", "hot.ee", "iCloud", "mail.ee", "qiye.aliyun"', ph: '示例:QQ', value: '' },
5757
{ key: 'SMTP_USER', desc: '邮件通知邮箱用户名。', ph: '示例:blog@imaegoo.com', value: '' },
5858
{ key: 'SMTP_PASS', desc: '邮件通知邮箱密码,QQ邮箱请填写授权码。', ph: '示例:password', value: '', secret: true }
5959
]
@@ -107,7 +107,6 @@ export default {
107107

108108
<style scoped>
109109
.tk-admin-config-groups {
110-
max-height: 600px;
111110
overflow-y: auto;
112111
padding-right: 0.5em;
113112
}

0 commit comments

Comments
 (0)