Skip to content

Commit d965eb4

Browse files
authored
feat: anti spam by captcha & blocked words (#636)
1 parent ddde134 commit d965eb4

File tree

19 files changed

+171
-19
lines changed

19 files changed

+171
-19
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,5 @@ docs/.vitepress/cache
117117
# database
118118
db.json
119119
db.json.*
120+
121+
pnpm-lock.yaml

demo/demo.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
<div class="field">
7575
<label class="label">语言 | Language: <code>lang</code></label>
7676
<div class="control">
77-
<input class="input" id="lang" type="text" placeholder="e.g en-US" value="en-US">
77+
<input class="input" id="lang" type="text" placeholder="e.g en-US" value="zh-CN">
7878
</div>
7979
</div>
8080
<div class="field">

docs/.vitepress/theme/Twikoo.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,6 @@ onMounted(() => {
7373

7474
<!-- Twikoo -->
7575
<div id="twikoo"></div>
76-
<component :is="'script'" src="https://cdn.jsdelivr.net/npm/twikoo@1.6.27/dist/twikoo.min.js" ref="twikooJs"></component>
76+
<component :is="'script'" src="https://cdn.jsdelivr.net/npm/twikoo@1.6.28/dist/twikoo.min.js" ref="twikooJs"></component>
7777
</div>
7878
</template>

docs/backend.md

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

4949
## 腾讯云命令行部署

docs/frontend.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ twikoo:
8585

8686
``` html
8787
<div id="tcomment"></div>
88-
<script src="https://cdn.staticfile.org/twikoo/1.6.27/twikoo.all.min.js"></script>
88+
<script src="https://cdn.staticfile.org/twikoo/1.6.28/twikoo.all.min.js"></script>
8989
<script>
9090
twikoo.init({
9191
envId: '您的环境id', // 腾讯云环境填 envId;Vercel 环境填地址(https://xxx.vercel.app)
@@ -103,10 +103,10 @@ twikoo.init({
103103

104104
如果遇到默认 CDN 加载速度缓慢,可更换其他 CDN 镜像。以下为可供选择的公共 CDN,其中一些 CDN 可能需要数天时间同步最新版本:
105105

106-
* `https://cdn.staticfile.org/twikoo/1.6.27/twikoo.all.min.js`
107-
* `https://lib.baomitu.com/twikoo/1.6.27/twikoo.all.min.js`
108-
* `https://cdn.bootcdn.net/ajax/libs/twikoo/1.6.27/twikoo.all.min.js`
109-
* `https://cdn.jsdelivr.net/npm/twikoo@1.6.27/dist/twikoo.all.min.js`
106+
* `https://cdn.staticfile.org/twikoo/1.6.28/twikoo.all.min.js`
107+
* `https://lib.baomitu.com/twikoo/1.6.28/twikoo.all.min.js`
108+
* `https://cdn.bootcdn.net/ajax/libs/twikoo/1.6.28/twikoo.all.min.js`
109+
* `https://cdn.jsdelivr.net/npm/twikoo@1.6.28/dist/twikoo.all.min.js`
110110

111111
## 开启管理面板(腾讯云环境)
112112

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": "1.6.27",
3+
"version": "1.6.28",
44
"description": "A simple comment system.",
55
"keywords": [
66
"twikoojs",

src/client/utils/i18n/i18n.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,14 @@ export default {
415415
'Тақиқланган сўзларни созланг. Тақиқланган сўзларни ўз ичига олган шарҳлар автоматик равишда спамга юборилади. Вергул билан ажратинг.',
416416
'禁止語設定、禁止語を含むコンテンツは直ちにスパムコメントとしてマークされます。コンマで区切ってください。'
417417
],
418+
[S.ACI + '_BLOCKED_WORDS']: [
419+
'屏蔽词配置,包含屏蔽词的内容会直接评论失败。英文逗号分隔。',
420+
'屏蔽词配置,包含屏蔽词的内容会直接评论失败。英文逗号分隔。',
421+
'屏蔽词配置,包含屏蔽词的内容会直接评论失败。英文逗号分隔。',
422+
'Configure blocked words. Comments containing blocked words will fail to send. Separate by comma.',
423+
'Configure blocked words. Comments containing blocked words will fail to send. Separate by comma.',
424+
'Configure blocked words. Comments containing blocked words will fail to send. Separate by comma.'
425+
],
418426
[S.ACI + '_GRAVATAR_CDN']: [
419427
'自定义头像 CDN 地址。如:cn.gravatar.com, cravatar.cn, sdn.geekzu.org, gravatar.loli.net,默认:cravatar.cn',
420428
'自定義頭像 CDN 地址。如:cn.gravatar.com, cravatar.cn, sdn.geekzu.org, gravatar.loli.net,預設:cravatar.cn',
@@ -535,6 +543,22 @@ export default {
535543
'Спам шарҳлар учун билдиришномалар. Стандарт: рост.',
536544
'スパムコメントの通知を送信するかどうか、デフォルト:true'
537545
],
546+
[S.ACI + '_TURNSTILE_SITE_KEY']: [
547+
'Turnstile 验证码的站点密钥。申请地址: https://dash.cloudflare.com/?to=/:account/turnstile',
548+
'Turnstile 验证码的站点密钥。申请地址: https://dash.cloudflare.com/?to=/:account/turnstile',
549+
'Turnstile 验证码的站点密钥。申请地址: https://dash.cloudflare.com/?to=/:account/turnstile',
550+
'Turnstile CAPTCHA Site Key. Get from: https://dash.cloudflare.com/?to=/:account/turnstile',
551+
'Turnstile CAPTCHA Site Key. Get from: https://dash.cloudflare.com/?to=/:account/turnstile',
552+
'Turnstile CAPTCHA Site Key. Get from: https://dash.cloudflare.com/?to=/:account/turnstile'
553+
],
554+
[S.ACI + '_TURNSTILE_SECRET_KEY']: [
555+
'Turnstile 验证码的密钥',
556+
'Turnstile 验证码的密钥',
557+
'Turnstile 验证码的密钥',
558+
'Turnstile CAPTCHA Secret Key',
559+
'Turnstile CAPTCHA Secret Key',
560+
'Turnstile CAPTCHA Secret Key'
561+
],
538562
[S.ACI + '_QCLOUD_SECRET_ID']: [
539563
'腾讯云 secret id,用于垃圾评论检测。同时设置腾讯云和 Akismet 时,只有腾讯云会生效。注册:https://twikoo.js.org/cms.html',
540564
'騰訊雲 secret id,用於垃圾評論檢測。同時設定騰訊雲和 Akismet 時,只有騰訊雲會生效。註冊:https://twikoo.js.org/cms.html',

src/client/version.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
const version = '1.6.27'
1+
const version = '1.6.28'
22

33
export { version }

src/client/view/components/TkAdminConfig.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ export default {
9595
{ key: 'LIMIT_PER_MINUTE_ALL', desc: t('ADMIN_CONFIG_ITEM_LIMIT_PER_MINUTE_ALL'), ph: `${t('ADMIN_CONFIG_EXAMPLE')}5`, value: '' },
9696
{ key: 'LIMIT_LENGTH', desc: t('ADMIN_CONFIG_ITEM_LIMIT_LENGTH'), ph: `${t('ADMIN_CONFIG_EXAMPLE')}100`, value: '' },
9797
{ key: 'FORBIDDEN_WORDS', desc: t('ADMIN_CONFIG_ITEM_FORBIDDEN_WORDS'), ph: `${t('ADMIN_CONFIG_EXAMPLE')}快递,空包`, value: '' },
98-
{ key: 'NOTIFY_SPAM', desc: t('ADMIN_CONFIG_ITEM_NOTIFY_SPAM'), ph: `${t('ADMIN_CONFIG_EXAMPLE')}false`, value: '' }
98+
{ key: 'BLOCKED_WORDS', desc: t('ADMIN_CONFIG_ITEM_BLOCKED_WORDS'), ph: `${t('ADMIN_CONFIG_EXAMPLE')}快递,空包`, value: '' },
99+
{ key: 'NOTIFY_SPAM', desc: t('ADMIN_CONFIG_ITEM_NOTIFY_SPAM'), ph: `${t('ADMIN_CONFIG_EXAMPLE')}false`, value: '' },
100+
{ key: 'TURNSTILE_SITE_KEY', desc: t('ADMIN_CONFIG_ITEM_TURNSTILE_SITE_KEY'), ph: `${t('ADMIN_CONFIG_EXAMPLE')}0x4AAAAAAAPLTtpBr_T12345`, value: '' },
101+
{ key: 'TURNSTILE_SECRET_KEY', desc: t('ADMIN_CONFIG_ITEM_TURNSTILE_SECRET_KEY'), ph: `${t('ADMIN_CONFIG_EXAMPLE')}0x4AAAAAAAPLTmBm6gHmOnOqC1iwmU12345`, value: '', secret: true }
99102
]
100103
},
101104
{

src/client/view/components/TkSubmit.vue

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@
4141
size="small"
4242
:disabled="!canSend"
4343
@click="send">{{ isSending ? t('SUBMIT_SENDING') : t('SUBMIT_SEND') }}</el-button>
44+
<div class="tk-turnstile-container" ref="turnstile-container">
45+
<div class="tk-turnstile" id="tk-turnstile"></div>
46+
</div>
4447
</div>
4548
<div class="tk-preview-container" v-if="isPreviewing" v-html="commentHtml" ref="comment-preview"></div>
4649
</div>
@@ -94,6 +97,7 @@ export default {
9497
nick: '',
9598
mail: '',
9699
link: '',
100+
turnstileLoad: null,
97101
iconMarkdown,
98102
iconEmotion,
99103
iconImage
@@ -144,6 +148,32 @@ export default {
144148
marked.setOptions({ odata: initMarkedOwo(odata) })
145149
}
146150
},
151+
initTurnstile () {
152+
if (!this.config.TURNSTILE_SITE_KEY) return
153+
this.turnstileLoad = new Promise((resolve, reject) => {
154+
const scriptEl = document.createElement('script')
155+
scriptEl.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'
156+
scriptEl.onload = resolve
157+
scriptEl.onerror = reject
158+
this.$refs['turnstile-container'].appendChild(scriptEl)
159+
})
160+
},
161+
getTurnstileToken () {
162+
return new Promise((resolve, reject) => {
163+
this.turnstileLoad.then(() => {
164+
const widgetId = window.turnstile.render('#tk-turnstile', {
165+
sitekey: this.config.TURNSTILE_SITE_KEY,
166+
callback: (token) => {
167+
resolve(token)
168+
setTimeout(() => {
169+
window.turnstile.remove(widgetId)
170+
}, 5000)
171+
},
172+
'error-callback': reject
173+
})
174+
})
175+
})
176+
},
147177
onMetaUpdate (updates) {
148178
this.nick = updates.meta.nick
149179
this.mail = updates.meta.mail
@@ -190,6 +220,9 @@ export default {
190220
pid: this.pid ? this.pid : this.replyId,
191221
rid: this.replyId
192222
}
223+
if (this.config.TURNSTILE_SITE_KEY) {
224+
comment.turnstileToken = await this.getTurnstileToken()
225+
}
193226
const sendResult = await call(this.$tcb, 'COMMENT_SUBMIT', comment)
194227
if (sendResult && sendResult.result && sendResult.result.id) {
195228
this.comment = ''
@@ -346,6 +379,9 @@ export default {
346379
},
347380
'config.COMMENT_BG_IMG': function () {
348381
this.onBgImgChange()
382+
},
383+
'config.TURNSTILE_SITE_KEY': function () {
384+
this.initTurnstile()
349385
}
350386
}
351387
}
@@ -412,6 +448,15 @@ export default {
412448
background-position: right bottom;
413449
background-repeat: no-repeat;
414450
}
451+
.tk-turnstile-container {
452+
position: absolute;
453+
right: 0;
454+
bottom: -75px;
455+
}
456+
.tk-turnstile {
457+
display: flex;
458+
flex-direction: column;
459+
}
415460
.tk-preview-container {
416461
margin-left: 3rem;
417462
margin-bottom: 1rem;

src/server/function/twikoo/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const {
2727
getQQAvatar,
2828
getPasswordStatus,
2929
preCheckSpam,
30+
checkTurnstileCaptcha,
3031
getConfig,
3132
getConfigForAdmin,
3233
validate
@@ -563,6 +564,8 @@ async function commentSubmit (event, context) {
563564
validate(event, ['url', 'ua', 'comment'])
564565
// 限流
565566
await limitFilter()
567+
// 验证码
568+
await checkCaptcha(event)
566569
// 预检测、转换
567570
const data = await parse(event)
568571
// 保存
@@ -675,6 +678,16 @@ async function limitFilter () {
675678
}
676679
}
677680

681+
async function checkCaptcha (comment) {
682+
if (config.TURNSTILE_SITE_KEY && config.TURNSTILE_SECRET_KEY) {
683+
await checkTurnstileCaptcha({
684+
ip: auth.getClientIP(),
685+
turnstileToken: comment.turnstileToken,
686+
turnstileTokenSecretKey: config.TURNSTILE_SECRET_KEY
687+
})
688+
}
689+
}
690+
678691
async function saveSpamCheckResult (comment, isSpam) {
679692
comment.isSpam = isSpam
680693
if (isSpam) {

src/server/function/twikoo/package.json

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

src/server/function/twikoo/utils/index.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const { URL } = require('url')
2-
const { axios, bowser, ipToRegion, md5 } = require('./lib')
2+
const { axios, FormData, bowser, ipToRegion, md5 } = require('./lib')
33
const { RES_CODE } = require('./constants')
44
const ipRegionSearcher = ipToRegion.create() // 初始化 IP 属地
55
const logger = require('./logger')
@@ -210,6 +210,16 @@ const fn = {
210210
if (limitLength && comment.length > limitLength) {
211211
throw new Error('评论内容过长')
212212
}
213+
if (config.BLOCKED_WORDS) {
214+
const commentLowerCase = comment.toLowerCase()
215+
const nickLowerCase = nick.toLowerCase()
216+
for (const blockedWord of config.BLOCKED_WORDS.split(',')) {
217+
const blockedWordLowerCase = blockedWord.trim().toLowerCase()
218+
if (commentLowerCase.indexOf(blockedWordLowerCase) !== -1 || nickLowerCase.indexOf(blockedWordLowerCase) !== -1) {
219+
throw new Error('包含屏蔽词')
220+
}
221+
}
222+
}
213223
if (config.AKISMET_KEY === 'MANUAL_REVIEW') {
214224
// 人工审核
215225
logger.info('已使用人工审核模式,评论审核后才会发表~')
@@ -228,6 +238,21 @@ const fn = {
228238
}
229239
return false
230240
},
241+
async checkTurnstileCaptcha ({ ip, turnstileToken, turnstileTokenSecretKey }) {
242+
try {
243+
const formData = new FormData()
244+
formData.append('secret', turnstileTokenSecretKey)
245+
formData.append('response', turnstileToken)
246+
formData.append('remoteip', ip)
247+
const { data } = await axios.post('https://challenges.cloudflare.com/turnstile/v0/siteverify', formData, {
248+
headers: formData.getHeaders()
249+
})
250+
logger.log('验证码检测结果', data)
251+
if (!data.success) throw new Error('验证码错误')
252+
} catch (e) {
253+
throw new Error('验证码检测失败: ' + e.message)
254+
}
255+
},
231256
async getConfig ({ config, VERSION, isAdmin }) {
232257
return {
233258
code: RES_CODE.SUCCESS,
@@ -250,7 +275,8 @@ const fn = {
250275
HIDE_ADMIN_CRYPT: config.HIDE_ADMIN_CRYPT,
251276
HIGHLIGHT: config.HIGHLIGHT || 'true',
252277
HIGHLIGHT_THEME: config.HIGHLIGHT_THEME,
253-
LIMIT_LENGTH: config.LIMIT_LENGTH
278+
LIMIT_LENGTH: config.LIMIT_LENGTH,
279+
TURNSTILE_SITE_KEY: config.TURNSTILE_SITE_KEY
254280
}
255281
}
256282
},

src/server/netlify/package.json

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

src/server/self-hosted/index.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const {
3232
getQQAvatar,
3333
getPasswordStatus,
3434
preCheckSpam,
35+
checkTurnstileCaptcha,
3536
getConfig,
3637
getConfigForAdmin,
3738
validate
@@ -603,6 +604,8 @@ async function commentSubmit (event, request) {
603604
validate(event, ['url', 'ua', 'comment'])
604605
// 限流
605606
await limitFilter(request)
607+
// 验证码
608+
await checkCaptcha(event, request)
606609
// 预检测、转换
607610
const data = await parse(event, request)
608611
// 保存
@@ -709,6 +712,16 @@ async function limitFilter (request) {
709712
}
710713
}
711714

715+
async function checkCaptcha (comment, request) {
716+
if (config.TURNSTILE_SITE_KEY && config.TURNSTILE_SECRET_KEY) {
717+
await checkTurnstileCaptcha({
718+
ip: getIp(request),
719+
turnstileToken: comment.turnstileToken,
720+
turnstileTokenSecretKey: config.TURNSTILE_SECRET_KEY
721+
})
722+
}
723+
}
724+
712725
async function saveSpamCheckResult (comment, isSpam) {
713726
comment.isSpam = isSpam
714727
if (isSpam) {

src/server/self-hosted/mongo.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const {
3030
getQQAvatar,
3131
getPasswordStatus,
3232
preCheckSpam,
33+
checkTurnstileCaptcha,
3334
getConfig,
3435
getConfigForAdmin,
3536
validate
@@ -585,6 +586,8 @@ async function commentSubmit (event, request) {
585586
validate(event, ['url', 'ua', 'comment'])
586587
// 限流
587588
await limitFilter(request)
589+
// 验证码
590+
await checkCaptcha(event, request)
588591
// 预检测、转换
589592
const data = await parse(event, request)
590593
// 保存
@@ -691,6 +694,16 @@ async function limitFilter (request) {
691694
}
692695
}
693696

697+
async function checkCaptcha (comment, request) {
698+
if (config.TURNSTILE_SITE_KEY && config.TURNSTILE_SECRET_KEY) {
699+
await checkTurnstileCaptcha({
700+
ip: getIp(request),
701+
turnstileToken: comment.turnstileToken,
702+
turnstileTokenSecretKey: config.TURNSTILE_SECRET_KEY
703+
})
704+
}
705+
}
706+
694707
async function saveSpamCheckResult (comment, isSpam) {
695708
comment.isSpam = isSpam
696709
if (isSpam) {

src/server/self-hosted/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tkserver",
3-
"version": "1.6.27",
3+
"version": "1.6.28",
44
"description": "A simple comment system.",
55
"keywords": [
66
"twikoo",
@@ -31,7 +31,7 @@
3131
"get-user-ip": "^1.0.1",
3232
"lokijs": "^1.5.12",
3333
"mongodb": "^3.6.3",
34-
"twikoo-func": "1.6.27",
34+
"twikoo-func": "1.6.28",
3535
"uuid": "^8.3.2"
3636
}
3737
}

0 commit comments

Comments
 (0)