├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github
└── workflows
│ ├── build.yml
│ ├── docs.yml
│ └── publish.yml
├── .gitignore
├── .idea
├── .gitignore
├── modules.xml
├── twikoo.iml
└── vcs.xml
├── .npmignore
├── CHANGELOG.md
├── Dockerfile
├── LICENSE
├── README.en.md
├── README.md
├── babel.config.json
├── cloudbaserc.json
├── demo
├── demo.css
├── demo.html
└── index.html
├── docker-compose.yml
├── docs
├── .vitepress
│ ├── config.ts
│ └── theme
│ │ ├── Layout.vue
│ │ ├── Twikoo.vue
│ │ └── index.ts
├── QQ_API.md
├── api.md
├── backend.md
├── cms.md
├── configuration.md
├── en
│ ├── api.md
│ ├── backend.md
│ ├── faq.md
│ ├── frontend.md
│ ├── index.md
│ ├── intro.md
│ ├── link.md
│ ├── quick-start.md
│ └── update.md
├── faq.md
├── frontend.md
├── index.md
├── intro.md
├── link.md
├── mongodb-atlas.md
├── package.json
├── public
│ ├── twikoo-logo-home.png
│ └── twikoo-logo-mini.png
├── quick-start.md
├── static
│ ├── faq-1.png
│ ├── hugging-1.png
│ ├── hugging-2.png
│ ├── hugging-3.png
│ ├── hugging-4.png
│ ├── hugging-5.png
│ ├── hugging-6.png
│ ├── hugging-7.png
│ ├── hugging-8.png
│ ├── hugging-9.png
│ ├── katex.png
│ ├── logo.png
│ ├── mongodb-1.png
│ ├── mongodb-2.png
│ ├── mongodb-3.png
│ ├── netlify-1.png
│ ├── netlify-2.png
│ ├── netlify-3.png
│ ├── netlify-4.png
│ ├── netlify-5.png
│ ├── readme-1.png
│ ├── readme-2.png
│ ├── readme-3.jpg
│ └── vercel-1.png
└── update.md
├── package.json
├── src
├── client
│ ├── lib
│ │ ├── marked
│ │ │ ├── Lexer.js
│ │ │ ├── Parser.js
│ │ │ ├── README.md
│ │ │ ├── Renderer.js
│ │ │ ├── Slugger.js
│ │ │ ├── TextRenderer.js
│ │ │ ├── Tokenizer.js
│ │ │ ├── defaults.js
│ │ │ ├── helpers.js
│ │ │ ├── marked.js
│ │ │ └── rules.js
│ │ ├── owo.css
│ │ └── owo.js
│ ├── main.all.js
│ ├── main.js
│ ├── utils
│ │ ├── api.js
│ │ ├── avatar.js
│ │ ├── emotion.js
│ │ ├── highlight.js
│ │ ├── i18n
│ │ │ ├── i18n.js
│ │ │ └── index.js
│ │ ├── index.js
│ │ ├── marked.js
│ │ ├── tcb.js
│ │ └── timeago.js
│ ├── version.js
│ └── view
│ │ ├── App.vue
│ │ ├── components
│ │ ├── TkAction.vue
│ │ ├── TkAdmin.vue
│ │ ├── TkAdminComment.vue
│ │ ├── TkAdminConfig.vue
│ │ ├── TkAdminExport.vue
│ │ ├── TkAdminImport.vue
│ │ ├── TkAvatar.vue
│ │ ├── TkComment.vue
│ │ ├── TkComments.vue
│ │ ├── TkFooter.vue
│ │ ├── TkMetaInput.vue
│ │ ├── TkPagination.vue
│ │ └── TkSubmit.vue
│ │ └── index.js
└── server
│ ├── aws-lambda
│ ├── LICENSE
│ ├── README.md
│ ├── src
│ │ ├── index.js
│ │ └── package.json
│ └── terraform
│ │ ├── .gitignore
│ │ ├── main.tf
│ │ └── variables.tf
│ ├── deta
│ ├── README.md
│ ├── Spacefile
│ ├── index.js
│ └── package.json
│ ├── function
│ ├── README.md
│ └── twikoo
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── package.json
│ │ └── utils
│ │ ├── constants.js
│ │ ├── image.js
│ │ ├── import.js
│ │ ├── index.js
│ │ ├── lib.js
│ │ ├── logger.js
│ │ ├── notify.js
│ │ └── spam.js
│ ├── hf-space
│ ├── Dockerfile
│ ├── README.md
│ └── src
│ │ └── start.sh
│ ├── netlify
│ ├── LICENSE
│ ├── README.md
│ ├── netlify
│ │ └── functions
│ │ │ └── twikoo.js
│ └── package.json
│ ├── pkg
│ ├── .env
│ ├── .eslintignore
│ ├── .gitignore
│ ├── .npmrc
│ ├── .vscode
│ │ └── settings.json
│ ├── README.md
│ ├── index.js
│ ├── package.json
│ ├── patches
│ │ ├── pkg-fetch+3.4.2.patch
│ │ └── tkserver+1.6.31.patch
│ └── web.config
│ ├── self-hosted
│ ├── README.md
│ ├── index.js
│ ├── mongo.js
│ ├── package.json
│ └── server.js
│ ├── vercel-min
│ ├── .github
│ │ └── dependabot.yml
│ ├── api
│ │ └── index.js
│ ├── package.json
│ └── vercel.json
│ └── vercel
│ ├── LICENSE
│ ├── README.md
│ ├── api
│ └── index.js
│ ├── package.json
│ └── vercel.json
├── webpack.config.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [!{node_modules}/**]
4 | end_of_line = lf
5 | charset = utf-8
6 |
7 | [{*.js,*.json,*.vue,*.html,*.css}]
8 | indent_style = space
9 | indent_size = 2
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.css
2 | *.json
3 | *.md
4 | *.sh
5 | *.tf
6 | LICENSE
7 | src/client/lib/marked/*
8 | src/server/self-hosted/data/*
9 | yarn.lock
10 | src/server/pkg/dist/*
11 | src/server/pkg/patches/*
12 | src/server/pkg/web.config
13 | pnpm-lock.yaml
14 | Dockerfile
15 | Spacefile
16 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true
6 | },
7 | extends: [
8 | 'plugin:vue/essential',
9 | 'standard'
10 | ],
11 | parserOptions: {
12 | ecmaVersion: 12,
13 | sourceType: 'module'
14 | },
15 | plugins: [
16 | 'vue'
17 | ],
18 | rules: {
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Build
5 |
6 | on:
7 | push:
8 | branches: [ main, dev ]
9 | pull_request:
10 | branches: [ main, dev ]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | strategy:
16 | matrix:
17 | node-version: [18.x]
18 | steps:
19 | - uses: actions/checkout@v2
20 | - name: Use Node.js ${{ matrix.node-version }}
21 | uses: actions/setup-node@v1
22 | with:
23 | node-version: ${{ matrix.node-version }}
24 | - run: yarn install
25 | - run: yarn build
26 | - run: yarn lint
27 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3 |
4 | name: Docs
5 |
6 | on:
7 | push:
8 | branches:
9 | - main
10 | paths:
11 | - 'docs/**'
12 | workflow_dispatch:
13 |
14 | jobs:
15 | publish-docs:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | # VuePress 需要完整提交历史来生成贡献者信息
20 | with:
21 | fetch-depth: 0
22 | - uses: imaegoo/vuepress-deploy@master
23 | env:
24 | ACCESS_TOKEN: ${{ secrets.TWIKOO_TOKEN }}
25 | TARGET_REPO: twikoojs/twikoo
26 | TARGET_BRANCH: gh-pages
27 | BUILD_SCRIPT: cd docs && yarn && yarn docs:build
28 | BUILD_DIR: .vitepress/dist
29 | CNAME: twikoo.js.org
30 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | release:
5 | types: [published]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | publish-npm:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: actions/setup-node@v1
14 | with:
15 | node-version: 18
16 | registry-url: https://registry.npmjs.org/
17 | - run: yarn install
18 | - run: yarn build
19 | - if: "github.event.release.prerelease"
20 | run: yarn publish --tag beta
21 | env:
22 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
23 | - if: "github.event.release.prerelease"
24 | run: cd src/server/function/twikoo && yarn publish --tag beta
25 | env:
26 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
27 | - if: "github.event.release.prerelease"
28 | run: cd src/server/vercel && yarn publish --tag beta
29 | env:
30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
31 | - if: "github.event.release.prerelease"
32 | run: cd src/server/self-hosted && yarn publish --tag beta
33 | env:
34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
35 | - if: "github.event.release.prerelease"
36 | run: cd src/server/netlify && yarn publish --tag beta
37 | env:
38 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
39 | - if: "!github.event.release.prerelease"
40 | run: yarn publish
41 | env:
42 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
43 | - if: "!github.event.release.prerelease"
44 | run: cd src/server/function/twikoo && yarn publish
45 | env:
46 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
47 | - if: "!github.event.release.prerelease"
48 | run: cd src/server/vercel && yarn publish
49 | env:
50 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
51 | - if: "!github.event.release.prerelease"
52 | run: cd src/server/self-hosted && yarn publish
53 | env:
54 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
55 | - if: "!github.event.release.prerelease"
56 | run: cd src/server/netlify && yarn publish
57 | env:
58 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
59 | publish-docker:
60 | needs: publish-npm
61 | runs-on: ubuntu-latest
62 | steps:
63 | -
64 | name: Checkout
65 | uses: actions/checkout@v3
66 | -
67 | name: Set up QEMU
68 | uses: docker/setup-qemu-action@v2
69 | -
70 | name: Set up Docker Buildx
71 | uses: docker/setup-buildx-action@v2
72 | -
73 | name: Login to DockerHub
74 | uses: docker/login-action@v2
75 | with:
76 | username: ${{ secrets.DOCKER_USERNAME }}
77 | password: ${{ secrets.DOCKER_PASSWORD }}
78 | -
79 | name: Get twikoo:latest version
80 | run: echo "TWIKOO_LATEST_VERSION=$(npm view twikoo@latest version)" >> "$GITHUB_ENV"
81 | -
82 | name: Build and push amd64 image
83 | uses: docker/build-push-action@v3
84 | with:
85 | context: .
86 | platforms: linux/amd64,linux/arm64
87 | push: true
88 | tags: |
89 | imaegoo/twikoo:latest
90 | imaegoo/twikoo:${{ env.TWIKOO_LATEST_VERSION }}
91 | -
92 | name: Build and push arm32v7 image
93 | uses: docker/build-push-action@v3
94 | with:
95 | context: .
96 | platforms: linux/arm/v7
97 | push: true
98 | tags: |
99 | imaegoo/twikoo:arm32v7
100 | imaegoo/twikoo:${{ env.TWIKOO_LATEST_VERSION }}-arm32v7
101 | build-args: NODE_IMAGE=arm32v7/node
102 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | stats.json
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 | package-lock.json
45 |
46 | # TypeScript v1 declaration files
47 | typings/
48 |
49 | # TypeScript cache
50 | *.tsbuildinfo
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional eslint cache
56 | .eslintcache
57 |
58 | # Microbundle cache
59 | .rpt2_cache/
60 | .rts2_cache_cjs/
61 | .rts2_cache_es/
62 | .rts2_cache_umd/
63 |
64 | # Optional REPL history
65 | .node_repl_history
66 |
67 | # Output of 'npm pack'
68 | *.tgz
69 |
70 | # Yarn Integrity file
71 | .yarn-integrity
72 |
73 | # dotenv environment variables file
74 | .env
75 | .env.test
76 |
77 | # parcel-bundler cache (https://parceljs.org/)
78 | .cache
79 |
80 | # Next.js build output
81 | .next
82 |
83 | # Nuxt.js build / generate output
84 | .nuxt
85 | dist
86 |
87 | # Gatsby files
88 | .cache/
89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
90 | # https://nextjs.org/blog/next-9-1#public-directory-support
91 | # public
92 |
93 | # vuepress build output
94 | docs/.vuepress/dist
95 | docs/.vitepress/dist
96 | docs/.vitepress/cache
97 |
98 | # Serverless directories
99 | .serverless/
100 |
101 | # FuseBox cache
102 | .fusebox/
103 |
104 | # DynamoDB Local files
105 | .dynamodb/
106 |
107 | # TernJS port file
108 | .tern-port
109 |
110 | # Vercel env file
111 | .vercel
112 |
113 | # VuePress temp file
114 | .temp
115 |
116 | # database
117 | db.json
118 | db.json.*
119 |
120 | pnpm-lock.yaml
121 | .idea/
122 |
123 | # macOS
124 | .DS_Store
125 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # 默认忽略的文件
2 | /shelf/
3 | /workspace.xml
4 | # 基于编辑器的 HTTP 客户端请求
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/twikoo.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.lock
2 | *.log
3 | .DS_Store
4 | .babelrc
5 | .editorconfig
6 | .env
7 | .eslintignore
8 | .eslintrc.js
9 | .github
10 | .gitignore
11 | .npmignore
12 | _config.js
13 | babel.config.json
14 | cloudbaserc.json
15 | demo
16 | dist/demo.css
17 | dist/demo.html
18 | dist/index.html
19 | docs
20 | node_modules
21 | src
22 | stats.json
23 | webpack.config.js
24 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 更新日志 | Release notes
2 |
3 | [https://github.com/twikoojs/twikoo/releases](https://github.com/twikoojs/twikoo/releases)
4 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG NODE_IMAGE=node
2 | FROM ${NODE_IMAGE}:lts AS build
3 | WORKDIR /app
4 | ENV NODE_ENV=production
5 | RUN set -eux; \
6 | npm install --production tkserver@latest; \
7 | mkdir -p data
8 | FROM ${NODE_IMAGE}:lts-alpine
9 | WORKDIR /app
10 | ENV NODE_ENV=production
11 | COPY --from=build /app .
12 | EXPOSE 8080
13 | CMD ["/app/node_modules/.bin/tkserver"]
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-present iMaeGoo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ----
4 |
5 | [](https://www.npmjs.com/package/twikoo)
6 | [](https://bundlephobia.com/result?p=twikoo)
7 | [](https://www.npmjs.com/package/twikoo)
8 | [](https://www.jsdelivr.com/package/npm/twikoo)
9 | [](./LICENSE)
10 |
11 |
12 | A **simple**, **safe**, **free** comment system.
13 | [简体中文](./README.md) | **English**
14 |
15 | **This document is for American English.**
16 |
17 | ## Features
18 |
19 |
20 | Click to view.
21 |
22 | ### Simple
23 |
24 | * Free Build.(Using Tencent CloudBase as the commenting backend, each user enjoys 1 free standard basic version 1 resource package for a long time)
25 | * Simple Deployment.(Support one-click deployment, manual deployment, command deployment)
26 |
27 | ### Easy to use
28 |
29 | * Support reply, like.
30 | * No additional adaptations, support with light theme and dark theme use.
31 | * Support API , batch get article comment count, latest comments.
32 | * Visitors entering QQ number in the nickname field will automatically complete the QQ nickname and QQ email.
33 | * Visitors fill in the digital QQ e-mail, will use the QQ avatar as the comment avatar.
34 | * Support the comment to paste pictures.(Can be disabled)
35 | * Support inserting pictures.(Can be disabled)
36 | * Support 7bu image bed, Tencent CloudBase image bed.
37 | * Support inserting emoji.(Can be disabled)
38 | * Support Ctrl + Enter reply.
39 | * Comments are saved in draft in real time and will not be lost when refreshed.
40 | * [Support Katex formulas.](https://twikoo.js.org/faq.html#%E5%A6%82%E4%BD%95%E5%90%AF%E7%94%A8-katex-%E6%94%AF%E6%8C%81)
41 | * Support for code highlighting by language.
42 |
43 | ### Security
44 |
45 | * Privacy and information security. (sensitive fields (email, IP, environment configuration, etc.) are not leaked through Tencent cloud function control)
46 | * Support for Akismet spam comment detection.(View Details [akismet.com](https://akismet.com/))
47 | * Support Tencent Cloud content security spam comment detection.(View Details [Tencent Cloud Content Security](https://console.cloud.tencent.com/cms/text/overview))
48 | * Support manual review mode.
49 | * Anti XSS Attack.
50 | * Support for limiting the maximum number of comments per IP per 10 minutes.
51 |
52 | ### notification
53 |
54 | * E-mail(Visitors and Blogger)
55 | * Wechat(only Blogger, [Server酱](https://sc.ftqq.com/3.version))
56 | * QQ(only Blogger, [Qmsg酱](https://qmsg.zendee.cn/))
57 |
58 | ### Personalization
59 |
60 | * Background image.
61 | * the "blogger" logo text.
62 | * Notification Email Template.
63 | * Comment prompt message.(placeholder)
64 | * emoji([OwO 的数据格式](https://cdn.jsdelivr.net/npm/owo@1.0.2/demo/OwO.json))
65 | * 【Nickname】 【Email】 【Website】 Required / Optional
66 | * Code highlighting theme.
67 |
68 | ### Management
69 |
70 | * Embedded panel with password login to easily view comments, hide comments, delete comments and modify configuration.
71 | * Support to hide the management portal and show it by entering a secret code.
72 | * Support for importing comments from Valine, Artalk, Disqus.
73 |
74 | ### Disadvantages
75 |
76 | * Slower requests. (except China)
77 | * Deployment requires real name authentication.
78 | * IE is not supported.
79 |
80 |
81 |
82 | ## Preview
83 |
84 |
85 | Click to view.
86 |
87 | ### Comments
88 |
89 | 
90 |
91 | ### Management
92 |
93 | 
94 |
95 | ### Notification
96 |
97 | 
98 |
99 |
100 |
101 | ## Quick Start
102 |
103 | [](https://console.cloud.tencent.com/tcb/env/index?action=CreateAndDeployCloudBaseProject&appUrl=https%3A%2F%2Fgithub.com%2Fimaegoo%2Ftwikoo&branch=main)
104 |
105 | [View Details](https://twikoo.js.org/quick-start.html)
106 |
107 |
108 | If you want to get updates, make suggestions and participate in the test, welcome to join the discussion group:1080829142 (QQ)
109 |
110 |
111 |
112 |
113 |
114 | ## Special Thanks
115 |
116 | Icon design:[Maemo Lee](https://www.maemo.cc)
117 |
118 |
119 |
120 | ## Release notes & plans
121 |
122 | [Update logs](https://github.com/twikoojs/twikoo/releases) & [Development Plan](https://github.com/twikoojs/twikoo/projects/2)
123 |
124 | ## Development
125 |
126 | If you want to develop locally for a second time, you can refer to the following commands:
127 |
128 | ``` sh
129 | yarn dev # (http://localhost:9820/demo.html)
130 | yarn lint
131 | yarn build # (dist/twikoo.all.min.js)
132 | ```
133 |
134 | If your changes can help more people, feel free to submit a Pull Request!
135 |
136 | ## I18N
137 |
138 | Support Simplified Chinese, Traditional Chinese, English. [translate Pull Request](https://github.com/twikoojs/twikoo/tree/main/src/client/utils/i18n).
139 |
140 | ## License
141 |
142 |
143 | MIT License
144 |
145 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fimaegoo%2Ftwikoo?ref=badge_large)
146 |
147 |
148 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ----
4 |
5 | [](https://www.npmjs.com/package/twikoo)
6 | [](https://bundlephobia.com/result?p=twikoo)
7 | [](https://www.npmjs.com/package/twikoo)
8 | [](https://www.jsdelivr.com/package/npm/twikoo)
9 | [](./LICENSE)
10 |
11 | 一个**简洁**、**安全**、**免费**的静态网站评论系统。
12 | A **simple**, **safe**, **free** comment system.
13 | **简体中文** | [English](./README.en.md)
14 |
15 | ## 特色 | Features
16 |
17 |
18 | 点击展开
19 |
20 | ### 简单
21 |
22 | * 免费搭建(使用云开发 / Vercel / 私有部署评论后台)
23 | * 简单部署(支持云开发 / Vercel 一键部署)
24 |
25 | ### 易用
26 |
27 | * 支持回复、点赞
28 | * 无需额外适配,支持搭配浅色主题与深色主题使用
29 | * 支持 API 调用,批量获取文章评论数、最新评论
30 | * 访客在昵称栏输入 QQ 号,会自动补全 QQ 昵称和 QQ 邮箱
31 | * 访客填写数字 QQ 邮箱,会使用 QQ 头像作为评论头像
32 | * 支持评论框粘贴图片(可禁用)
33 | * 支持插入图片(可禁用)
34 | * 支持去不图床、云开发图床
35 | * 支持插入表情(可禁用)
36 | * 支持 Ctrl + Enter 快捷回复
37 | * 评论框内容实时保存草稿,刷新不会丢失
38 | * [支持 Katex 公式](https://twikoo.js.org/faq.html#%E5%A6%82%E4%BD%95%E5%90%AF%E7%94%A8-katex-%E6%94%AF%E6%8C%81)
39 | * 支持按语言的代码高亮
40 |
41 | ### 安全
42 |
43 | * 隐私信息安全(通过云函数控制敏感字段(邮箱、IP、环境配置等)不会泄露)
44 | * 支持 Akismet 垃圾评论检测(需自行注册 [akismet.com](https://akismet.com/))
45 | * 支持腾讯云内容安全垃圾评论检测(需自行注册 [腾讯云内容安全](https://console.cloud.tencent.com/cms/text/overview))
46 | * 支持人工审核模式
47 | * 防 XSS 注入
48 | * 支持限制每个 IP 每 10 分钟最多发表多少条评论
49 |
50 | ### 即时
51 |
52 | * 支持邮件提醒(访客和博主)
53 | * 支持微信提醒(仅针对博主,基于 [Server酱](https://sc.ftqq.com/3.version),需自行注册)
54 | * 支持 QQ 提醒(仅针对博主,基于 [Qmsg酱](https://qmsg.zendee.cn/),需自行注册)
55 |
56 | ### 个性
57 |
58 | * 支持自定义评论框背景图片
59 | * 支持自定义“博主”标识文字
60 | * 支持自定义通知邮件模板
61 | * 支持自定义评论框提示信息(placeholder)
62 | * 支持自定义表情列表(兼容 [OwO 的数据格式](https://cdn.jsdelivr.net/npm/owo@1.0.2/demo/OwO.json))
63 | * 支持自定义【昵称】【邮箱】【网址】必填 / 选填
64 | * 支持自定义代码高亮主题
65 |
66 | ### 便捷管理
67 |
68 | * 内嵌式管理面板,通过密码登录,可方便地查看评论、隐藏评论、删除评论、修改配置
69 | * 支持隐藏管理入口,通过输入暗号显示
70 | * 支持从 Valine、Artalk、Disqus 导入评论
71 |
72 | ### 缺点
73 |
74 | * 不支持 IE
75 |
76 |
77 |
78 | ## 预览 | Preview
79 |
80 |
81 | 点击展开
82 |
83 | ### 评论
84 |
85 | 
86 |
87 | ### 评论管理
88 |
89 | 
90 |
91 | ### 推送通知
92 |
93 | 
94 |
95 |
96 |
97 | ## 快速上手 | Quick Start
98 |
99 | 有关详细教程,请查看[快速上手](https://twikoo.js.org/quick-start.html)
100 |
101 |
102 | 如果你想获取更新动态、建言献策、参与测试,欢迎加入讨论群:1080829142
103 |
104 |
105 |
106 |
107 |
108 | ## 特别感谢 | Special Thanks
109 |
110 | 图标设计:[Maemo Lee](https://www.maemo.cc)
111 |
112 |
113 |
114 | ## 开发 | Development
115 |
116 | 如果您想在本地二次开发,可以参考以下命令:
117 |
118 | ``` sh
119 | yarn dev # 开发 (http://localhost:9820/demo.html)
120 | yarn lint # 代码检查
121 | yarn build # 编译 (dist/twikoo.all.min.js)
122 | ```
123 |
124 | 如果您的改动能够帮助到更多人,欢迎提交 Pull Request!
125 |
126 | ## 国际化 | I18N
127 |
128 | 支持简体中文、繁体中文、English。欢迎[提交翻译 PR](https://github.com/twikoojs/twikoo/edit/main/src/client/utils/i18n/i18n.js)。
129 |
130 | ## 许可 | License
131 |
132 |
133 | MIT License
134 |
135 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fimaegoo%2Ftwikoo?ref=badge_large)
136 |
137 |
138 |
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env"],
3 | "plugins": ["@babel/plugin-transform-modules-commonjs", "@babel/transform-runtime"]
4 | }
--------------------------------------------------------------------------------
/cloudbaserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "envId": "{{envId}}",
4 | "functionRoot": "./src/server/function",
5 | "functions": [
6 | {
7 | "name": "twikoo",
8 | "timeout": 30,
9 | "runtime": "Nodejs16.13",
10 | "memorySize": 128,
11 | "handler": "index.main"
12 | }
13 | ],
14 | "framework": {
15 | "name": "twikoo",
16 | "plugins": {
17 | "function": {
18 | "use": "@cloudbase/framework-plugin-function",
19 | "inputs": {
20 | "functionRootPath": "./src/server/function",
21 | "functions": [
22 | {
23 | "name": "twikoo",
24 | "timeout": 30,
25 | "envVariables": {},
26 | "runtime": "Nodejs16.13",
27 | "memory": 128
28 | }
29 | ],
30 | "servicePaths": {
31 | "twikoo": "/twikoo"
32 | }
33 | }
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/demo/demo.css:
--------------------------------------------------------------------------------
1 | .section {
2 | background-color: hsl(0, 0%, 96%);
3 | }
4 | .footer {
5 | background: none;
6 | }
7 |
8 | /* 以下 CSS 用于夜间测试 */
9 | /* .card {
10 | color: #eeeeee;
11 | background-color: #222222;
12 | }
13 | .card strong{
14 | color: currentColor;
15 | }
16 | .card pre {
17 | color: currentColor;
18 | background-color: transparent;
19 | } */
20 |
--------------------------------------------------------------------------------
/demo/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Twikoo Demo
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
45 |
46 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | N/A
96 |
97 |
98 |
99 |
100 |
101 |
104 |
105 |
112 |
113 |
114 |
115 |
126 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # https://docs.docker.com/compose/compose-file/compose-file-v3/
2 | version: '3'
3 | services:
4 | twikoo-service:
5 | image: imaegoo/twikoo
6 | environment:
7 | TWIKOO_THROTTLE: 250
8 | TZ: Asia/Shanghai
9 | volumes:
10 | - /app/data
11 | ports:
12 | - 8080:8080
13 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 |
3 | // https://vitepress.dev/reference/site-config
4 | export default defineConfig({
5 | head: [
6 | ['link', { rel: 'icon', href: '/twikoo-logo-mini.png' }],
7 | ['meta', { name: 'theme-color', content: '#007aff' }]
8 | ],
9 | locales: {
10 | root: {
11 | label: '简体中文',
12 | lang: 'zh-CN',
13 | link: '/',
14 | title: 'Twikoo 文档',
15 | description: '一个简洁、安全、免费的静态网站评论系统',
16 | themeConfig: {
17 | sidebar: [
18 | { text: '简介', link: '/intro' },
19 | { text: '快速上手', link: '/quick-start' },
20 | { text: 'MongoDB Atlas', link: '/mongodb-atlas' },
21 | { text: '云函数部署', link: '/backend' },
22 | { text: '前端部署', link: '/frontend' },
23 | { text: '版本更新', link: '/update' },
24 | { text: '常见问题', link: '/faq' },
25 | { text: 'API 文档', link: '/api' },
26 | { text: '相关文档', link: '/link' }
27 | ],
28 | editLink: {
29 | pattern: 'https://github.com/twikoojs/twikoo/edit/main/docs/:path',
30 | text: '在 GitHub 上编辑此页面'
31 | },
32 | footer: {
33 | message: '基于 MIT 许可发布',
34 | copyright: `版权所有 © 2020 至今 iMaeGoo`
35 | },
36 | docFooter: {
37 | prev: '上一篇',
38 | next: '下一篇'
39 | },
40 | outline: {
41 | label: '本页导航'
42 | },
43 | lastUpdated: {
44 | text: '最后更新于',
45 | formatOptions: {
46 | dateStyle: 'short',
47 | timeStyle: 'medium'
48 | }
49 | },
50 | langMenuLabel: '多语言',
51 | returnToTopLabel: '回到顶部',
52 | sidebarMenuLabel: '菜单',
53 | darkModeSwitchLabel: '主题',
54 | lightModeSwitchTitle: '切换到浅色模式',
55 | darkModeSwitchTitle: '切换到深色模式'
56 | }
57 | },
58 | en: {
59 | label: 'English (US)',
60 | lang: 'en',
61 | link: '/en/',
62 | title: 'Twikoo Docs',
63 | description: 'A simple, safe, free comment system',
64 | themeConfig: {
65 | sidebar: [
66 | { text: 'Introduction', link: '/en/intro' },
67 | { text: 'Quick start', link: '/en/quick-start' },
68 | { text: 'FAQ', link: '/en/faq' },
69 | { text: 'API', link: '/en/api' }
70 | ]
71 | }
72 | }
73 | },
74 | themeConfig: {
75 | logo: {
76 | src: '/twikoo-logo-mini.png',
77 | width: 24,
78 | height: 24
79 | },
80 | search: {
81 | provider: 'algolia',
82 | options: {
83 | appId: 'TM627WNO90',
84 | apiKey: 'f81194a47bc4be7984df25fc480c60a7',
85 | indexName: 'twikoo'
86 | }
87 | },
88 | socialLinks: [
89 | { icon: 'github', link: 'https://github.com/twikoojs/twikoo' }
90 | ],
91 | editLink: {
92 | pattern: 'https://github.com/twikoojs/twikoo/edit/main/docs/:path'
93 | },
94 | footer: {
95 | message: 'Released under the MIT License.',
96 | copyright: 'Copyright © 2020-present iMaeGoo'
97 | }
98 | },
99 | lastUpdated: true
100 | })
101 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/Layout.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
50 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/Twikoo.vue:
--------------------------------------------------------------------------------
1 |
62 |
63 |
64 |
78 |
79 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import DefaultTheme from 'vitepress/theme'
2 | import Layout from './Layout.vue'
3 |
4 | export default {
5 | ...DefaultTheme,
6 | // override the Layout with a wrapper component that
7 | // injects the slots
8 | Layout
9 | }
10 |
--------------------------------------------------------------------------------
/docs/QQ_API.md:
--------------------------------------------------------------------------------
1 | # QQ私有化部署文档
2 | ## 1. 下载go-cq
3 | 前往[go-cqhttp release](https://github.com/Mrs4s/go-cqhttp)下载对应系统版本。
4 | ## 2. 配置服务
5 | 解压
6 |
7 | > Windows下请使用自己熟悉的解压软件自行解压
8 |
9 | > Linux下在命令行中输入tar -xzvf [文件名]
10 | 使用
11 |
12 | **Windows 标准方法**
13 |
14 | 双击,根据提示生成运行脚本go-cqhttp_*.exe
15 |
16 | `[WARNING]: 尝试加载配置文件 config.yml 失败: 文件不存在
17 | [INFO]: 默认配置文件已生成,请编辑 config.yml 后重启程序.`
18 |
19 | 配置文件请参考下方config.yml
20 |
21 |
22 | config.yml配置好后 再次双击运行脚本
23 |
24 | `[INFO]: 登录成功 欢迎使用: balabala`
25 |
26 | 如出现需要认证的信息, 请自行认证设备。
27 |
28 | 此时, 基础配置完成
29 |
30 | **Linux 标准方法**
31 |
32 |
33 | 通过 SSH 连接到服务器
34 |
35 | cd到解压目录
36 |
37 | 输入 , 运行 `./go-cqhttp`
38 |
39 |
40 | `[WARNING]: 尝试加载配置文件 config.yml 失败: 文件不存在
41 | [INFO]: 默认配置文件已生成,请编辑 config.yml 后重启程序.`
42 |
43 |
44 | **配置config.yml**
45 |
46 | ```yaml
47 |
48 | # go-cqhttp 默认配置文件
49 |
50 | account: # 账号相关
51 | uin: # QQ账号
52 | password: '' # 密码为空时使用扫码登录
53 | encrypt: false # 是否开启密码加密
54 | status: 0 # 在线状态 请参考 https://docs.go-cqhttp.org/guide/config.html#在线状态
55 | relogin: # 重连设置
56 | delay: 3 # 首次重连延迟, 单位秒
57 | interval: 3 # 重连间隔
58 | max-times: 0 # 最大重连次数, 0为无限制
59 |
60 | # 是否使用服务器下发的新地址进行重连
61 | # 注意, 此设置可能导致在海外服务器上连接情况更差
62 | use-sso-address: true
63 |
64 | heartbeat:
65 | # 心跳频率, 单位秒
66 | # -1 为关闭心跳
67 | interval: 5
68 |
69 | message:
70 | # 上报数据类型
71 | # 可选: string,array
72 | post-format: string
73 | # 是否忽略无效的CQ码, 如果为假将原样发送
74 | ignore-invalid-cqcode: false
75 | # 是否强制分片发送消息
76 | # 分片发送将会带来更快的速度
77 | # 但是兼容性会有些问题
78 | force-fragment: false
79 | # 是否将url分片发送
80 | fix-url: false
81 | # 下载图片等请求网络代理
82 | proxy-rewrite: ''
83 | # 是否上报自身消息
84 | report-self-message: false
85 | # 移除服务端的Reply附带的At
86 | remove-reply-at: false
87 | # 为Reply附加更多信息
88 | extra-reply-data: false
89 |
90 | output:
91 | # 日志等级 trace,debug,info,warn,error
92 | log-level: warn
93 | # 是否启用 DEBUG
94 | debug: false # 开启调试模式
95 |
96 | # 默认中间件锚点
97 | default-middlewares: &default
98 | # 访问密钥, 强烈推荐在公网的服务器设置
99 | access-token: ''
100 | # 事件过滤器文件目录
101 | filter: ''
102 | # API限速设置
103 | # 该设置为全局生效
104 | # 原 cqhttp 虽然启用了 rate_limit 后缀, 但是基本没插件适配
105 | # 目前该限速设置为令牌桶算法, 请参考:
106 | # https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
107 | rate-limit:
108 | enabled: true # 是否启用限速
109 | frequency: 1 # 令牌回复频率, 单位秒
110 | bucket: 1 # 令牌桶大小
111 |
112 | database: # 数据库相关设置
113 | leveldb:
114 | # 是否启用内置leveldb数据库
115 | # 启用将会增加10-20MB的内存占用和一定的磁盘空间
116 | # 关闭将无法使用 撤回 回复 get_msg 等上下文相关功能
117 | enable: true
118 |
119 | # 连接服务列表
120 | servers:
121 | # 添加方式,同一连接方式可添加多个,具体配置说明请查看文档
122 | #- http: # http 通信
123 | #- ws: # 正向 Websocket
124 | #- ws-reverse: # 反向 Websocket
125 | #- pprof: #性能分析服务器
126 | # HTTP 通信设置
127 | - http:
128 | # 服务端监听地址
129 | host: 127.0.0.1
130 | # 服务端监听端口
131 | port: 5700
132 | # 反向HTTP超时时间, 单位秒
133 | # 最小值为5,小于5将会忽略本项设置
134 | timeout: 5
135 | middlewares:
136 | <<: *default # 引用默认中间件
137 | # 反向HTTP POST地址列表
138 | post:
139 | #- url: '' # 地址
140 | # secret: '' # 密钥
141 | #- url: 127.0.0.1:5701 # 地址
142 | # secret: '' # 密钥
143 |
144 |
145 |
146 |
147 |
148 | ```
149 |
150 |
151 | 再次运行`./go-cqhttp`
152 |
153 |
154 | `[INFO]: 登录成功 欢迎使用: balabala`
155 |
156 | 如出现需要认证的信息, 请自行认证设备。
157 |
158 | 此时, 基础配置完成
159 |
160 | ## 注意:将你配置的端口号在防火墙里面放行或者使用反向代理将你设置的端口绑定到域名
161 | ## 注意:公网使用一定要配置token
162 |
163 | ## 3. twikoo配置
164 | 在twikoo中QQ私有化API配置项填写如下内容
165 |
166 | QQ号
167 | `http://你的IP地址:端口号(或者域名)/send_private_msg?user_id=QQ号?token=你配置的token`
168 |
169 | QQ群
170 | `http://你的IP地址:端口号(或者域名)/send_group_msg?token=你配置的token?group_id=群号`
171 |
172 | 配置完成
173 |
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API 文档
2 |
3 | 通过 Twikoo API,主题开发者可以实现一些特殊的功能,例如:在文章列表显示文章评论数,在首页显示最新评论,等。
4 |
5 | 调用 Twikoo API 前,**不需要** 执行 `twikoo.init()`。
6 |
7 | ## Get comments count
8 |
9 | 批量获取文章评论数。
10 |
11 | ### Version
12 |
13 | `>= 0.2.7`
14 |
15 | ### Example
16 |
17 | ``` js
18 | twikoo.getCommentsCount({
19 | envId: '您的环境id', // 环境 ID
20 | // region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,如果您的环境地域不是上海,需传此参数
21 | urls: [ // 不包含协议、域名、参数的文章路径列表,必传参数
22 | '/2020/10/post-1.html',
23 | '/2020/11/post-2.html',
24 | '/2020/12/post-3.html'
25 | ],
26 | includeReply: false // 评论数是否包括回复,默认:false
27 | }).then(function (res) {
28 | console.log(res);
29 | // 返回示例: [
30 | // { url: '/2020/10/post-1.html', count: 10 },
31 | // { url: '/2020/11/post-2.html', count: 0 },
32 | // { url: '/2020/12/post-3.html', count: 20 }
33 | // ]
34 | }).catch(function (err) {
35 | // 发生错误
36 | console.error(err);
37 | });
38 | ```
39 |
40 | ## Get recent comments
41 |
42 | 获取最新评论。
43 |
44 | ### Version
45 |
46 | `>= 0.2.7`
47 |
48 | ### Example
49 |
50 | ``` js
51 | twikoo.getRecentComments({
52 | envId: '您的环境id', // 环境 ID
53 | // region: 'ap-guangzhou', // 环境地域,默认为 ap-shanghai,如果您的环境地域不是上海,需传此参数
54 | urls: [ // 要求云函数版本 >= 1.6.27。不包含协议、域名、参数的文章路径列表,不传默认获取所有最新评论
55 | '/2020/10/post-1.html',
56 | '/2020/11/post-2.html',
57 | '/2020/12/post-3.html'
58 | ],
59 | pageSize: 10, // 获取多少条,默认:10,最大:100
60 | includeReply: false // 是否包括最新回复,默认:false
61 | }).then(function (res) {
62 | console.log(res);
63 | // 返回 Array,包含最新评论的
64 | // * id: 评论 ID
65 | // * url: 评论地址
66 | // * nick: 昵称
67 | // * mailMd5: 邮箱的 MD5 值,可用于展示头像
68 | // * link: 网址
69 | // * comment: HTML 格式的评论内容
70 | // * commentText: 纯文本格式的评论内容
71 | // * created: 评论时间,格式为毫秒级时间戳
72 | // * avatar: 头像地址(0.2.9 新增)
73 | // * relativeTime: 相对评论时间,如 “1 小时前”(0.2.9 新增)
74 | // 返回示例: [ // 从新到旧顺序
75 | // { id: '', url: '', nick: '', mailMd5: '', link: '', comment: '', commentText: '', created: 0 },
76 | // { id: '', url: '', nick: '', mailMd5: '', link: '', comment: '', commentText: '', created: 0 },
77 | // { id: '', url: '', nick: '', mailMd5: '', link: '', comment: '', commentText: '', created: 0 }
78 | // ]
79 | }).catch(function (err) {
80 | // 发生错误
81 | console.error(err);
82 | });
83 | ```
84 |
85 | ## On Twikoo loaded
86 |
87 | Twikoo 成功挂载后的回调函数。
88 | 环境 ID 错误、网络异常、挂载失败等情况时不会触发。
89 |
90 | ### Version
91 |
92 | `>= 0.5.2`
93 |
94 | ### Example
95 |
96 | ``` js
97 | twikoo.init({
98 | ......
99 | }).then(function () {
100 | console.log('Twikoo 加载完成');
101 | });
102 | ```
103 |
104 | ## On comment loaded
105 |
106 | 评论加载成功后的回调函数。
107 | 发表评论后自动刷新评论时、加载下一页评论时,也会触发。
108 | 评论加载失败时不会触发。
109 |
110 | ### Version
111 |
112 | `>= 0.5.2`
113 |
114 | ### Example
115 |
116 | ``` js
117 | twikoo.init({
118 | ......,
119 | onCommentLoaded: function () {
120 | console.log('评论加载完成');
121 | }
122 | });
123 | ```
124 |
--------------------------------------------------------------------------------
/docs/cms.md:
--------------------------------------------------------------------------------
1 | # 反垃圾
2 |
3 | [如何配置反垃圾?](faq.html#如何配置反垃圾)
4 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | # 配置
2 |
3 | ::: warning 注意
4 | **因图形化配置界面已上线,此文档已废弃且不再维护,其中的内容可能已经过时**
5 |
6 | * 配置是可选的,即使没有配置也可以使用。
7 | * 请确保 config 表的权限**不是**“所有用户可读”,以保证 SMTP 密码等信息不会泄露。
8 | 不过放心,默认权限是安全的,您不需要更改。
9 | * 请将配置项放在一条数据记录中。
10 | :::
11 |
12 | ## 通用
13 |
14 | ### SITE_NAME
15 |
16 | 类型: `String`
17 | 默认值: `null`
18 | 必要性: `false`
19 | 示例: 虹墨空间站
20 |
21 | 博客、站点名称。
22 |
23 | ### SITE_URL
24 |
25 | 类型: `String`
26 | 默认值: `null`
27 | 必要性: `false`
28 | 示例: https://www.imaegoo.com
29 |
30 | 博客、站点地址。
31 |
32 | ### BLOGGER_EMAIL
33 |
34 | 类型: `String`
35 | 默认值: `null`
36 | 必要性: `false`
37 | 示例: 12345@qq.com
38 |
39 | 博主的邮箱地址,用于邮件通知、博主标识。
40 |
41 | ## 反垃圾
42 |
43 | ### AKISMET_KEY
44 |
45 | 类型: `String`
46 | 默认值: `null`
47 | 必要性: `false`
48 | 示例: 8651783ed123
49 |
50 | 反垃圾评论 API key。
51 |
52 | ## 微信通知
53 |
54 | ### SC_SENDKEY
55 |
56 | 类型: `String`
57 | 默认值: `null`
58 | 必要性: `false`
59 | 示例: SCT1364TKdsiGjGvyAZNYDVnuHW12345
60 |
61 | [Server酱](https://sc.ftqq.com/3.version)微信推送的 `SCKEY`
62 |
63 | ## 邮件通知
64 |
65 | ### SENDER_EMAIL
66 |
67 | 类型: `String`
68 | 默认值: `null`
69 | 必要性: `false`
70 | 示例: blog@imaegoo.com
71 |
72 | 邮件通知邮箱地址。对于大多数邮箱服务商,`SENDER_EMAIL` 必须和 `SMTP_USER` 保持一致,否则无法发送邮件。
73 |
74 | ### SENDER_NAME
75 |
76 | 类型: `String`
77 | 默认值: `null`
78 | 必要性: `false`
79 | 示例: 虹墨空间站评论提醒
80 |
81 | 邮件通知标题。
82 |
83 | ### SMTP_SERVICE
84 |
85 | 类型: `String`
86 | 默认值: `null`
87 | 必要性: `false`
88 | 示例: qiye.aliyun
89 |
90 | 邮件通知邮箱服务商。
91 | 完整列表请参考:[Supported services](https://nodemailer.com/smtp/well-known/#supported-services)
92 |
93 | ### SMTP_USER
94 |
95 | 类型: `String`
96 | 默认值: `null`
97 | 必要性: `false`
98 | 示例: blog@imaegoo.com
99 |
100 | 邮件通知邮箱用户名。
101 |
102 | ### SMTP_PASS
103 |
104 | 类型: `String`
105 | 默认值: `null`
106 | 必要性: `false`
107 | 示例: password
108 |
109 | 邮件通知邮箱密码,QQ邮箱请填写授权码。
110 |
--------------------------------------------------------------------------------
/docs/en/api.md:
--------------------------------------------------------------------------------
1 | # API reference
2 |
3 | Through Twikoo API, theme developers can implement some special features, such as displaying the number of article comments in the article list, displaying the latest comments on the home page, etc.
4 |
5 | It is **not necessary** to execute `twikoo.init()` before calling the Twikoo API.
6 |
7 | ## Get comments count
8 |
9 | Get the number of article comments in batch.
10 |
11 | ### Version
12 |
13 | `>= 0.2.7`
14 |
15 | ### Example
16 |
17 | ``` js
18 | twikoo.getCommentsCount({
19 | envId: 'Environment ID', // Tencent CloudBase Environment ID
20 | // region: 'ap-guangzhou', // Environment locale, default is ap-shanghai, if your environment locale is not Shanghai, you need to pass this parameter
21 | urls: [ // List of article paths without protocols, domains and parameters. It is a mandatory parameter
22 | '/2020/10/post-1.html',
23 | '/2020/11/post-2.html',
24 | '/2020/12/post-3.html'
25 | ],
26 | includeReply: false // Whether the number of comments includes replies, the default parameter is false
27 | }).then(function (res) {
28 | console.log(res);
29 | // example: [
30 | // { url: '/2020/10/post-1.html', count: 10 },
31 | // { url: '/2020/11/post-2.html', count: 0 },
32 | // { url: '/2020/12/post-3.html', count: 20 }
33 | // ]
34 | }).catch(function (err) {
35 | // If an error occurs
36 | console.error(err);
37 | });
38 | ```
39 |
40 | ## Get recent comments
41 |
42 | Get the latest comments.
43 |
44 | ### Version
45 |
46 | `>= 0.2.7`
47 |
48 | ### Example
49 |
50 | ``` js
51 | twikoo.getRecentComments({
52 | envId: '您的环境id', // Tencent CloudBase Environment ID
53 | // region: 'ap-guangzhou', // Environment locale, default is ap-shanghai, if your environment locale is not Shanghai, you need to pass this parameter
54 | pageSize: 10, // Get how many bars, the default parameter is 10, the maximum parameter is 100
55 | includeReply: false // Whether to include the latest reply, the default parameter is false
56 | }).then(function (res) {
57 | console.log(res);
58 | // Returns Array with the latest comments
59 | // * id: comment ID
60 | // * url: address of the comment
61 | // * nick: nickname
62 | // * mailMd5: The MD5 value of the mailbox, which can be used to display the avatar
63 | // * link: URL
64 | // * comment: the content of the comment in HTML format
65 | // * commentText: comment content in plain text format
66 | // * created: comment time, in millisecond timestamp format
67 | // * avatar: the address of the avatar (new in 0.2.9)
68 | // * relativeTime: relative comment time, e.g. "1 hour ago" (new in 0.2.9)
69 | // Return example: [ // order from new to old
70 | // { id: '', url: '', nick: '', mailMd5: '', link: '', comment: '', commentText: '', created: 0 }
71 | // { id: '', url: '', nick: '', mailMd5: '', link: '', comment: '', commentText: '', created: 0 },
72 | // { id: '', url: '', nick: '', mailMd5: '', link: '', comment: '', commentText: '', created: 0 }
73 | // ]
74 | }).catch(function (err) {
75 | // If an error occurs
76 | console.error(err);
77 | });
78 | ```
79 |
80 | ## On Twikoo loaded
81 |
82 | Callback function after Twikoo is successfully mounted.
It will not be triggered in case of environment ID error, network exception, mount failure, etc.
83 |
84 | ### Version
85 |
86 | `>= 0.5.2`
87 |
88 | ### Example
89 |
90 | ``` js
91 | twikoo.init({
92 | ......
93 | }).then(function () {
94 | console.log('Twikoo is ready to go!');
95 | });
96 | ```
97 |
98 | ## On comment loaded
99 |
100 | Callback function after comments are loaded successfully.
101 | It will also be triggered when the comment is automatically refreshed after posting and when the next page of comments is loaded.
102 | It will not be triggered when the comment fails to load.
103 |
104 | ### Version
105 |
106 | `>= 0.5.2`
107 |
108 | ### Example
109 |
110 | ``` js
111 | twikoo.init({
112 | ......,
113 | onCommentLoaded: function () {
114 | console.log('Comment loading complete');
115 | }
116 | });
117 | ```
118 |
--------------------------------------------------------------------------------
/docs/en/backend.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/en/backend.md
--------------------------------------------------------------------------------
/docs/en/frontend.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/en/frontend.md
--------------------------------------------------------------------------------
/docs/en/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 |
4 | title: Twikoo
5 | titleTemplate: A simple, safe, free comment system
6 |
7 | hero:
8 | name: Twikoo
9 | text: Comment system
10 | tagline: Simple, safe, free
11 | actions:
12 | - theme: brand
13 | text: Quick start
14 | link: /en/quick-start
15 | - theme: alt
16 | text: Introduction
17 | link: /en/intro
18 | - theme: alt
19 | text: View on GitHub
20 | link: https://github.com/twikoojs/twikoo
21 | image:
22 | src: /twikoo-logo-home.png
23 | alt: Twikoo
24 |
25 | features:
26 | - icon: 🚀
27 | title: 简单
28 | details: 免费搭建,简单部署
29 | - icon: 😃
30 | title: 易用
31 | details: 功能丰富,兼容性强
32 | - icon: 🛡️
33 | title: 安全
34 | details: 隐私安全,内容安全
35 | - icon: ⏰
36 | title: 即时
37 | details: 邮件提醒,即时消息推送
38 | - icon: 🌈
39 | title: 个性
40 | details: 自定义背景图,博主标识
41 | - icon: ⚙️
42 | title: 便捷管理
43 | details: 内嵌式管理面板,通过密码登录
44 | ---
45 |
--------------------------------------------------------------------------------
/docs/en/intro.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ----
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | A **simple**, **safe**, **free** comment system.
28 | [简体中文](/intro) | **English**
29 |
30 | **This document is for American English. This document has many bugs.**
31 |
32 | ## Features
33 |
34 |
35 | ### Simple
36 |
37 | * Free Build.(Using CloudBase / Vercel / self-hosted as the commenting backend)
38 | * Simple Deployment.(Support CloudBase / Vercel one-click deployment)
39 |
40 | ### Easy to use
41 |
42 | * Support reply, like.
43 | * No additional adaptations, support with light theme and dark theme use.
44 | * Support API , batch get article comment count, latest comments.
45 | * Visitors entering QQ number in the nickname field will automatically complete the QQ nickname and QQ email.
46 | * Visitors fill in the digital QQ e-mail, will use the QQ avatar as the comment avatar.
47 | * Support the comment to paste pictures.(Can be disabled)
48 | * Support inserting pictures.(Can be disabled)
49 | * Support 7bu image bed, Tencent CloudBase image bed.
50 | * Support inserting emoji.(Can be disabled)
51 | * Support Ctrl + Enter reply.
52 | * Comments are saved in draft in real time and will not be lost when refreshed.
53 | * [Support Katex formulas.](https://twikoo.js.org/faq.html#%E5%A6%82%E4%BD%95%E5%90%AF%E7%94%A8-katex-%E6%94%AF%E6%8C%81)
54 | * Support for code highlighting by language.
55 |
56 | ### Security
57 |
58 | * Privacy and information security. (sensitive fields (email, IP, environment configuration, etc.) are not leaked through Tencent cloud function control)
59 | * Support for Akismet spam comment detection.(View Details [akismet.com](https://akismet.com/))
60 | * Support Tencent Cloud content security spam comment detection.(View Details [Tencent Cloud Content Security](https://console.cloud.tencent.com/cms/text/overview))
61 | * Support manual review mode.
62 | * Anti XSS Attack.
63 | * Support for limiting the maximum number of comments per IP per 10 minutes.
64 |
65 | ### notification
66 |
67 | * E-mail(Visitors and Blogger)
68 | * Wechat(only Blogger, [Server酱](https://sc.ftqq.com/3.version))
69 | * QQ(only Blogger, [Qmsg酱](https://qmsg.zendee.cn/))
70 |
71 | ### Personalization
72 |
73 | * Background image.
74 | * the "blogger" logo text.
75 | * Notification Email Template.
76 | * Comment prompt message.(placeholder)
77 | * emoji([OwO 的数据格式](https://cdn.jsdelivr.net/npm/owo@1.0.2/demo/OwO.json))
78 | * 【Nickname】 【Email】 【Website】 Required / Optional
79 | * Code highlighting theme.
80 |
81 | ### Management
82 |
83 | * Embedded panel with password login to easily view comments, hide comments, delete comments and modify configuration.
84 | * Support to hide the management portal and show it by entering a secret code.
85 | * Support for importing comments from Valine, Artalk, Disqus.
86 |
87 | ### Disadvantages
88 |
89 | * IE is not supported.
90 |
91 | ## Preview
92 |
93 | ### Comments
94 |
95 | 
96 |
97 | ### Management
98 |
99 | 
100 |
101 | ### Notification
102 |
103 | 
104 |
105 |
106 |
107 | ## Quick Start
108 |
109 | [](https://console.cloud.tencent.com/tcb/env/index?action=CreateAndDeployCloudBaseProject&appUrl=https%3A%2F%2Fgithub.com%2Fimaegoo%2Ftwikoo&branch=main)
110 |
111 | [View Details](https://twikoo.js.org/quick-start.html)
112 |
113 |
114 | If you want to get updates, make suggestions and participate in the test, welcome to join the discussion group:1080829142 (QQ)
115 |
116 |
117 |
118 |
119 |
120 | ## Special Thanks
121 |
122 | Icon design:[Maemo Lee](https://www.maemo.cc)
123 |
124 |
125 |
126 | ## Release notes & plans
127 |
128 | [Update logs](https://github.com/twikoojs/twikoo/releases) & [Development Plan](https://github.com/twikoojs/twikoo/projects/2)
129 |
130 | ## Development
131 |
132 | If you want to develop locally for a second time, you can refer to the following commands:
133 |
134 | ``` sh
135 | yarn dev # (http://localhost:9820/demo.html)
136 | yarn lint
137 | yarn build # (dist/twikoo.all.min.js)
138 | ```
139 |
140 | If your changes can help more people, feel free to submit a Pull Request!
141 |
142 | ## I18N
143 |
144 | Support Simplified Chinese, Traditional Chinese, English. [translate Pull Request](https://github.com/twikoojs/twikoo/tree/main/src/client/utils/i18n).
145 |
146 | ## License
147 |
148 |
149 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fimaegoo%2Ftwikoo?ref=badge_large)
150 |
151 |
--------------------------------------------------------------------------------
/docs/en/link.md:
--------------------------------------------------------------------------------
1 | # Links
2 |
--------------------------------------------------------------------------------
/docs/en/quick-start.md:
--------------------------------------------------------------------------------
1 | # Quick start
2 |
3 | ::: tip Tip
4 | The English document is being built. Please refer to the [Chinese document](/quick-start).
5 | :::
6 |
--------------------------------------------------------------------------------
/docs/en/update.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/en/update.md
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # 常见问题
2 |
3 | ## 如何修改头像?
4 |
5 | 请前往 [https://weavatar.com](https://weavatar.com) 通过邮箱注册并设定头像,评论时,请留下相同的邮箱。
6 |
7 | 访客还可以通过输入数字 QQ 邮箱地址,使用 QQ 头像发表评论。
8 |
9 | ## 如何修改、重置管理员密码?
10 |
11 | 腾讯云请前往[云开发控制台](https://console.cloud.tencent.com/tcb/database/collection/config),Vercel 请前往 MongoDB,私有部署请直接编辑 `data/db.json.1`,编辑配置,删除 config.ADMIN_PASS 配置项,然后前往 Twikoo 管理面板重新设置密码。
12 |
13 | ## 如何获得管理面板的私钥文件?
14 |
15 | 1. 进入[环境-登录授权](https://console.cloud.tencent.com/tcb/env/login),点击“自定义登录”右边的“私钥下载”,下载私钥文件
16 | 2. 用文本编辑器打开私钥文件,复制全部内容
17 | 3. 点击评论窗口的“小齿轮”图标,粘贴私钥文件内容,并设置管理员密码
18 |
19 | ## 忘记暗号,无法进入管理面板怎么办?
20 |
21 | 在包含评论框的页面,打开浏览器开发者工具(Windows 下快捷键为 F12),点击 Network 标签,刷新一下页面,点击放大镜图标(Search),在出现的搜索栏中输入 `HIDE_ADMIN_CRYPT`,点击搜索栏旁边的刷新图标(Refresh),即可找到您的暗号。
22 |
23 | 
24 |
25 | 请注意,暗号并非管理面板的加密手段,仅用于向普通访客隐藏管理面板,请勿把暗号和管理面板的密码设置为相同的字符串。
26 |
27 | ## 如何开启文章访问量统计?
28 |
29 | 您可以在需要展示文章访问量的地方添加:
30 |
31 | ``` html
32 | 0
33 | ```
34 |
35 | 来展示访问量。暂不支持全站访问量统计。
36 |
37 | ## 如何启用 Katex 支持?
38 |
39 | Twikoo 支持 Katex 公式,但为了限制 Twikoo 的包大小,Twikoo 没有内置完整的 Katex,您需要[在页面中额外加载 katex.js](https://katex.org/docs/browser.html)。
40 |
41 | ``` html
42 |
43 |
44 |
45 |
46 |
47 | ```
48 |
49 | 载入后,您可以发送 `$$c = \pm\sqrt{a^2 + b^2}$$` 测试效果。
50 |
51 | 
52 |
53 | 您还可以在 `twikoo.init` 时传入自定义 katex 配置,详细配置请查看 [Katex Auto-render Extension](https://katex.org/docs/autorender.html)。
54 |
55 | ``` js
56 | twikoo.init({
57 | envId: '您的环境id',
58 | el: '#tcomment',
59 | katex: {
60 | delimiters: [
61 | { left: '$$', right: '$$', display: true },
62 | { left: '$', right: '$', display: false },
63 | { left: '\\(', right: '\\)', display: false },
64 | { left: '\\[', right: '\\]', display: true }
65 | ],
66 | throwOnError: false
67 | }
68 | });
69 | ```
70 |
71 | ## 如何配置反垃圾?
72 |
73 | ### 使用腾讯云内容安全服务
74 |
75 | Twikoo 支持接入腾讯云文本内容检测,使用深度学习技术,识别涉黄、涉政、涉恐等有害内容,同时支持用户配置词库,打击自定义的违规文本。
76 |
77 | 腾讯云文本内容检测是付费服务,提供 1 个月的免费试用,之后价格为 25 元/万条。如果您对反垃圾评论要求不高,也可以使用免费的 Akismet。
78 |
79 | 如何申请腾讯云文本内容检测
80 |
81 | 1. 访问[腾讯云控制台-文本内容安全](https://console.cloud.tencent.com/cms/text/overview),开通文本内容安全服务
82 | 2. 访问[腾讯云控制台-用户列表](https://console.cloud.tencent.com/cam),点击新建用户,点击快速创建
83 | 3. 输入用户名,访问方式选择“编程访问”,用户权限取消“AdministratorAccess”,只勾选“QcloudTMSFullAccess”
84 | 4. 点击“创建用户”
85 | 5. 复制“成功新建用户”页面的“SecretId”和“SecretKey”,到 Twikoo 管理面板“反垃圾”模块中配置
86 | 6. 测试反垃圾效果
87 |
88 | 成功后,站长可以在[腾讯云控制台-自定义库管理](https://console.cloud.tencent.com/cms/text/lib)配置自定义文本内容过滤。
89 |
90 | ### 使用 Akismet 反垃圾服务
91 |
92 | Akismet (Automattic Kismet) 是应用广泛的一个垃圾留言过滤系统,其作者是大名鼎鼎的 WordPress 创始人 Matt Mullenweg,Akismet 也是 WordPress 默认安装的插件,其使用非常广泛,设计目标便是帮助博客网站来过滤垃圾留言。
93 |
94 | 1. 注册 [akismet.com](https://akismet.com)
95 | 2. 选择 Akismet Personal 订阅,复制得到的 Akismet API Key,到 Twikoo 管理面板“反垃圾”模块中配置
96 |
97 | ### 如何测试 Akismet 反垃圾配置是否生效?
98 |
99 | 请填写 `viagra-test-123` 作为昵称,或填写 `akismet-guaranteed-spam@example.com` 作为邮箱,发表评论,这条评论将一定会被视为垃圾评论。
100 |
101 | 需要注意的是,由于 Akismet 服务响应速度较慢(大约 6 秒),影响用户体验,Twikoo 采取 “先放行,后检测” 的策略,垃圾评论会在发表后短暂可见。
102 |
103 | ## 登录管理面板遇到错误 AUTH_INVALID_CUSTOM_LOGIN_TICKET
104 |
105 | 一般是配置好登录私钥之后,又重新下载了登录私钥,导致之前配置的登录私钥失效了。
106 | 解决方法:到[云开发控制台](https://console.cloud.tencent.com/tcb/database/collection/config),数据库,删掉 config,然后重新配置私钥。
107 |
108 | ## 收不到提醒邮件?
109 |
110 | 如果是 Vercel 部署的云函数,请配置国外邮件服务商,避免被邮件服务商判定为垃圾邮件行为。如果是其他原因,请前往 Twikoo 管理面板,找到邮件测试功能,输入个人邮箱,根据测试结果排查原因。
111 |
112 | 如果是 Vercel 部署的云函数,邮件测试正常,但实际评论收不到任何即时消息通知 / 邮件通知,请打开 Vercel 云函数管理页面,进入 Settings - Deployment Protection,设置 Vercel Authentication 为 Disabled,并 Save。
113 |
114 | 
115 |
116 | 为了避免频繁检查邮箱带来的性能问题,邮件配置有 10 分钟左右的缓存,如果确定配置没有问题,但测试失败,可以等待 10 分钟后再测试。
117 |
118 | 由于博主发表评论时,不会通知博主,如果您想实际测试通知功能,请注销管理面板后用非博主邮箱发表或回复评论。
119 |
120 | ## Vercel、私有部署无法上传图片?
121 |
122 | 腾讯云环境自带云存储,所以腾讯云环境下可以直接上传图片,图片保存在云存储中。然而 Vercel 环境没有,上传图片功能依赖第三方图床,请在管理面板中配置图床,Twikoo 支持以下图床:
123 |
124 | | 图床 | 地址 | 特点 |
125 | | ---- | ---- | ---- |
126 | | qcloud | 无 | 腾讯云环境自带,可在云开发 - 云存储中查看 |
127 | | 7bu | https://7bu.top | 去不图床,由杜老师提供支持,无免费套餐 |
128 | | smms | https://sm.ms | SMMS 图床,有免费套餐,请自行注册账号,`IMAGE_CDN_TOKEN` 可在 [Dashboard](https://sm.ms/home/apitoken) 中获取 |
129 | | [lsky-pro](https://www.lsky.pro) | 私有部署 | 兰空图床 2.0 版本,`IMAGE_CDN` 请配置图床首页 URL 地址(如 `https://7bu.top`),`IMAGE_CDN_TOKEN` 获取方式请参考教程 [杜老师说图床:新版本去不图床 Token 的获取与清空](https://dusays.com/454/),获取到的 token 格式应为 `1\|1bJbwlqBfnggmOMEZqXT5XusaIwqiZjCDs7r1Ob5`) |
130 |
131 | ## 私有部署能连接自己的数据库吗?
132 |
133 | Twikoo 私有部署版默认使用内置数据库:LokiJS 数据库,支持的数据库容量大约为 1 GB,不需要连接外部数据库,数据存储在启动 twikoo 时所在目录下的 data 目录,您可以直接复制该目录以完成数据备份。
134 |
135 | 如果您有 MongoDB 实例,可以连接 MongoDB 作为外部数据库,只需配置环境变量 MONGODB_URI 为数据库连接地址即可,如:`mongodb://:@/`。
136 |
137 | ## 部署后遇到评论失败: 0,管理面板进不去?
138 |
139 | 在包含评论框的页面,打开浏览器开发者工具(Windows 下快捷键为 F12),点击 Console 标签,查找包含 twikoo 关键字的报错。
140 |
141 | 如果看到 ERR_BLOCKED_BY_CLIENT,请禁用浏览器去广告插件或将当前网站加入白名单,然后刷新重试。
142 |
143 | 如果看到 ERR_CONNECTION_CLOSED / ERR_CONNECTION_TIMED_OUT / ERR_CONNECTION_RESET,请检查自己所处的地区网络环境是否正常,能够连通云函数,部分地区无法访问 Vercel 等服务,请更换部署方式再试。
144 |
145 | 如果看到 `Access to XMLHttpRequest at 'https://tcb-api.tencentcloudapi.com/web?env=...' from origin '...' has been blocked by CORS policy...`:请检查前端 js 文件版本是否最新,并确保 envId 以 `https://` 开头。
146 |
147 | 如果看到 `Access to XMLHttpRequest at ... No 'Access-Control-Allow-Origin' header is present on the requested resource.`:请先访问一下 envId 查看云函数是否运行正常,如果没有运行正常的提示,请重新部署云函数,确保不要漏下任何步骤;如果提示运行正常,请本地启动网站(localhost)并访问管理面板-配置管理-通用,清空 `CORS_ALLOW_ORIGIN` 字段并保存,然后刷新重试。
148 |
149 | 如果看到其他错误,请 [提交 issue](https://github.com/twikoojs/twikoo/issues/new) 并附上错误信息。
150 |
--------------------------------------------------------------------------------
/docs/frontend.md:
--------------------------------------------------------------------------------
1 | # 前端部署
2 |
3 | ## 在 Hexo 中使用
4 |
5 | ### 在 [Hexo Butterfly](https://github.com/jerryc127/hexo-theme-butterfly) 主题使用
6 |
7 | 请参考 [Butterfly 安裝文檔(四) 主題配置-2](https://butterfly.js.org/posts/ceeb73f/#%E8%A9%95%E8%AB%96) 进行配置
8 |
9 | ### 在 [Hexo Keep](https://github.com/XPoet/hexo-theme-keep) 主题使用
10 |
11 | 请参考 [hexo-theme-keep/_config.yml](https://github.com/XPoet/hexo-theme-keep/blob/master/_config.yml) 进行配置
12 |
13 | ### 在 [Hexo Volantis](https://github.com/volantis-x/hexo-theme-volantis) 主题使用
14 |
15 | 请参考 [hexo-theme-volantis/_config.yml](https://github.com/volantis-x/hexo-theme-volantis/blob/master/_config.yml) 进行配置
16 |
17 | ### 在 [Hexo Ayer](https://github.com/Shen-Yu/hexo-theme-ayer) 主题使用
18 |
19 | 请参考 [hexo-theme-ayer/_config.yml](https://github.com/Shen-Yu/hexo-theme-ayer/blob/master/_config.yml) 进行配置
20 |
21 | ### 在 [Hexo NexT](https://github.com/next-theme/hexo-theme-next) 主题使用
22 |
23 | **暂不支持 NexT 8 以下的版本**,请先升级到 NexT 8。然后在 Hexo 项目根目录执行
24 |
25 | ``` sh
26 | # For NexT version >= 8.0.0 && < 8.4.0
27 | npm install hexo-next-twikoo@1.0.0
28 | # For NexT version >= 8.4.0
29 | npm install hexo-next-twikoo@1.0.3
30 | ```
31 |
32 | 然后在配置中添加
33 |
34 | ``` yml
35 | twikoo:
36 | enable: true
37 | visitor: true
38 | envId: xxxxxxxxxxxxxxx # 腾讯云环境填 envId;Vercel 环境填地址(https://xxx.vercel.app)
39 | # region: ap-guangzhou # 环境地域,默认为 ap-shanghai,腾讯云环境填 ap-shanghai 或 ap-guangzhou;Vercel 环境不填
40 | ```
41 |
42 | ### 在 [Hexo Matery](https://github.com/blinkfox/hexo-theme-matery) 主题使用
43 |
44 | 请参考 [hexo-theme-matery/_config.yml](https://github.com/blinkfox/hexo-theme-matery/blob/develop/_config.yml) 进行配置
45 |
46 | ### 在 [Hexo Icarus](https://github.com/ppoffice/hexo-theme-icarus) 主题使用
47 |
48 | 请参考 [基于腾讯云,给你的 Icarus 博客配上 Twikoo 评论系统](https://www.anzifan.com/post/icarus_to_candy_2/) by 异次元de机智君💯
49 |
50 | ### 在 [Hexo MengD(萌典)](https://github.com/lete114/hexo-theme-MengD) 主题使用
51 |
52 | 请参考 [hexo-theme-MengD/_config.yml](https://github.com/lete114/hexo-theme-MengD/blob/master/_config.yml) 进行配置
53 |
54 | ### 在 [hexo-theme-fluid](https://github.com/fluid-dev/hexo-theme-fluid) 主题使用
55 |
56 | 请参考 [配置指南-评论](https://hexo.fluid-dev.com/docs/guide/#%E8%AF%84%E8%AE%BA) 进行配置
57 |
58 | ### 在 [hexo-theme-cards](https://github.com/ChrAlpha/hexo-theme-cards) 主题使用
59 |
60 | 请参考 [hexo-theme-cards/_config.yml](https://github.com/ChrAlpha/hexo-theme-cards/blob/master/_config.yml) 进行配置
61 |
62 | ### 在 [maupassant-hexo](https://github.com/tufu9441/maupassant-hexo) 主题使用
63 |
64 | 请参考 [maupassant-hexo/_config.yml](https://github.com/tufu9441/maupassant-hexo/blob/master/_config.yml) 进行配置
65 |
66 | ### 在 [hexo-theme-redefine](https://github.com/EvanNotFound/hexo-theme-redefine) 主题使用
67 |
68 | 请参考 [Redefine 官方文档 #comment](https://redefine-docs.ohevan.com/docs/configuration-guide/comment#twikoo) 进行配置
69 |
70 | ### 在 [Hexo-Theme-Solitude](https://github.com/valor-x/hexo-theme-solitude) 主题使用
71 |
72 | 请参考 [Solitude 文档](https://solitude-docs.efu.me/comments/twikoo) 进行配置
73 |
74 | ## 在 Hugo 中使用
75 |
76 | ### 在 [hugo-theme-stack](https://github.com/CaiJimmy/hugo-theme-stack) 主题使用
77 |
78 | 请参考 [Comments | Stack](https://stack.jimmycai.com/config/comments) 和 [hugo-theme-stack/config.yaml#L83](https://github.com/CaiJimmy/hugo-theme-stack/blob/master/config.yaml#L83) 进行配置
79 |
80 | ### 在 [FixIt](https://github.com/hugo-fixit/FixIt) 主题使用
81 |
82 | 请参考 [入门篇 - FixIt #主题配置](https://fixit.lruihao.cn/zh-cn/documentation/basics/#theme-configuration) 和 [hugo-fixit/FixIt/config.toml#L613-L624](https://github.com/hugo-fixit/FixIt/blob/8bb2a35dcc4c54fc3e0fb968df063d6be1daabf3/config.toml#L613-L624) 进行配置
83 |
84 | ## 在 VitePress 中使用
85 |
86 | 请参考 [VitePress 集成 twikoo 参考解决方案](https://github.com/twikoojs/twikoo/issues/715) 进行配置。
87 |
88 | ## 通过 CDN 引入
89 |
90 | ::: tip 提示
91 | 如果您使用的博客主题不支持 Twikoo,并且您不知道如何引入 Twikoo,您可以向博客主题开发者提交适配请求
92 | :::
93 |
94 | ``` html
95 |
96 |
97 |
106 | ```
107 |
108 | ### 不同版本之间的区别
109 |
110 | * `twikoo.all.min.js`: 包含腾讯云云开发(tcb)的完整版本,如果您使用腾讯云云开发部署,请选择此版本
111 | * `twikoo.min.js`: 去除了腾讯云云开发(tcb)的精简版本,体积更小,适合所有非腾讯云云开发部署的用户
112 | * `twikoo.nocss.js`: 在完整版本的基础上剥离了样式,需要同时引入 `twikoo.css` 才能正常显示,适合想要魔改评论区样式的用户
113 |
114 | ### 更换 CDN 镜像
115 |
116 | 如果遇到默认 CDN 加载速度缓慢,可更换其他 CDN 镜像。以下为可供选择的公共 CDN,其中一些 CDN 可能需要数天时间同步最新版本:
117 |
118 | #### 推荐在中国使用
119 |
120 | * `https://registry.npmmirror.com/twikoo/1.6.44/files/dist/twikoo.min.js`
121 | * `https://s4.zstatic.net/npm/twikoo@1.6.44/dist/twikoo.min.js`
122 |
123 | #### 推荐在全球使用
124 |
125 | * `https://cdn.jsdelivr.net/npm/twikoo@1.6.44/dist/twikoo.min.js`
126 |
127 | #### 备用选项
128 |
129 | * `https://s4.zstatic.net/ajax/libs/twikoo/1.6.41/twikoo.min.js`
130 | * `https://lib.baomitu.com/twikoo/1.6.39/twikoo.min.js`
131 |
132 | ::: warning 注意
133 | 建议使用 CDN 引入 Twikoo 的用户在链接地址上锁定版本,以免将来 Twikoo 升级时受到非兼容性更新的影响。
134 | :::
135 |
136 | ::: warning 注意
137 | 建议使用 CDN 引入 Twikoo 的用户在代码中加入 [SRI](https://developer.mozilla.org/zh-CN/docs/Web/Security/Subresource_Integrity) 以确保完整性,例:
138 | ```html
139 |
143 | ```
144 | 其中 `integrity` 的值可以在 [SRI Hash Generator](https://www.srihash.org/) 查询。
145 | :::
146 |
147 | ## 开启管理面板(腾讯云环境)
148 |
149 | 1. 进入[环境-登录授权](https://console.cloud.tencent.com/tcb/env/login),点击“自定义登录”右边的“私钥下载”,下载私钥文件
150 | 2. 用文本编辑器打开私钥文件,复制全部内容
151 | 3. 点击评论窗口的“小齿轮”图标,粘贴私钥文件内容,并设置管理员密码
152 |
153 | 配置好登录私钥之后无需留存私钥文件,请勿再次下载登录私钥,否则会导致之前配置的登录私钥失效。
154 |
155 | ## 开启管理面板(非腾讯云环境)
156 |
157 | 点击评论窗口的“小齿轮”图标,设置管理员密码
158 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 |
4 | title: Twikoo
5 | titleTemplate: 一个简洁、安全、免费的静态网站评论系统
6 |
7 | hero:
8 | name: Twikoo
9 | text: 网站评论系统
10 | tagline: 简洁、安全、免费
11 | actions:
12 | - theme: brand
13 | text: 快速上手
14 | link: /quick-start
15 | - theme: alt
16 | text: 简介
17 | link: /intro
18 | - theme: alt
19 | text: QQ 群
20 | link: https://jq.qq.com/?_wv=1027&k=2l9ZGIoL
21 | - theme: alt
22 | text: GitHub
23 | link: https://github.com/twikoojs/twikoo
24 | image:
25 | src: /twikoo-logo-home.png
26 | alt: Twikoo
27 |
28 | features:
29 | - icon: 🚀
30 | title: 简单
31 | details: 免费搭建,简单部署
32 | - icon: 😃
33 | title: 易用
34 | details: 功能丰富,兼容性强
35 | - icon: 🛡️
36 | title: 安全
37 | details: 隐私安全,内容安全
38 | - icon: ⏰
39 | title: 即时
40 | details: 邮件提醒,即时消息推送
41 | - icon: 🌈
42 | title: 个性
43 | details: 自定义背景图,博主标识
44 | - icon: ⚙️
45 | title: 便捷管理
46 | details: 内嵌式管理面板,通过密码登录
47 | ---
48 |
--------------------------------------------------------------------------------
/docs/intro.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ----
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 一个简洁、安全、免费的静态网站评论系统。
28 | A simple, safe, free comment system.
29 |
30 | **简体中文** | [English](/en/intro)
31 |
32 | ## 特色
33 |
34 | ### 简单
35 |
36 | * 免费搭建(使用云开发 / Vercel / 私有服务器作为评论后台)
37 | * 简单部署(支持云开发 / Vercel 一键部署)
38 |
39 | ### 易用
40 |
41 | * 支持回复、点赞
42 | * 无需额外适配,支持搭配浅色主题与深色主题使用
43 | * 支持 API 调用,批量获取文章评论数、最新评论
44 | * 访客在昵称栏输入 QQ 号,会自动补全 QQ 昵称和 QQ 邮箱
45 | * 访客填写数字 QQ 邮箱,会使用 QQ 头像作为评论头像
46 | * 支持评论框粘贴图片(可禁用)
47 | * 支持插入图片(可禁用)
48 | * 支持去不图床、云开发图床
49 | * 支持插入表情(可禁用)
50 | * 支持 Ctrl + Enter 快捷回复
51 | * 评论框内容实时保存草稿,刷新不会丢失
52 | * [支持 Katex 公式](https://twikoo.js.org/faq.html#%E5%A6%82%E4%BD%95%E5%90%AF%E7%94%A8-katex-%E6%94%AF%E6%8C%81)
53 | * 支持按语言的代码高亮
54 |
55 | ### 安全
56 |
57 | * 隐私信息安全(通过云函数控制敏感字段(邮箱、IP、环境配置等)不会泄露)
58 | * 支持 Akismet 垃圾评论检测(需自行注册 [akismet.com](https://akismet.com/))
59 | * 支持腾讯云内容安全垃圾评论检测(需自行注册 [腾讯云内容安全](https://console.cloud.tencent.com/cms/text/overview))
60 | * 支持人工审核模式
61 | * 防 XSS 注入
62 | * 支持限制每个 IP 每 10 分钟最多发表多少条评论
63 |
64 | ### 即时
65 |
66 | * 支持邮件提醒(访客和博主)
67 | * 支持微信提醒(仅针对博主,基于 [Server酱](https://sc.ftqq.com/3.version),需自行注册)
68 | * 支持 QQ 提醒(仅针对博主,基于 [Qmsg酱](https://qmsg.zendee.cn/),需自行注册)
69 | * 支持 QQ 提醒(针对博主QQ或者群,基于 [go-cqhttp](https://docs.go-cqhttp.org/),需自己有服务器)
70 |
71 |
72 | ### 个性
73 |
74 | * 支持自定义评论框背景图片
75 | * 支持自定义“博主”标识文字
76 | * 支持自定义通知邮件模板
77 | * 支持自定义评论框提示信息(placeholder)
78 | * 支持自定义表情列表(兼容 [OwO 的数据格式](https://cdn.jsdelivr.net/npm/owo@1.0.2/demo/OwO.json))
79 | * 支持自定义【昵称】【邮箱】【网址】必填 / 选填
80 | * 支持自定义代码高亮主题
81 |
82 | ### 便捷管理
83 |
84 | * 内嵌式管理面板,通过密码登录,可方便地查看评论、隐藏评论、删除评论、修改配置
85 | * 支持隐藏管理入口,通过输入暗号显示
86 | * 支持从 Valine、Artalk、Disqus 导入评论
87 |
88 | ### 缺点
89 |
90 | * 不支持 IE
91 |
92 | ## 预览
93 |
94 | ### 评论
95 |
96 | 
97 |
98 | ### 评论管理
99 |
100 | 
101 |
102 | ### 推送通知
103 |
104 | 
105 |
106 | ## 交流群
107 |
108 | 如果你想获取更新动态、建言献策、参与测试,欢迎加入讨论群:
109 |
110 |
111 | ## 浏览器支持
112 |
113 | ::: tip 提示
114 | 技术原因,不兼容 IE
115 | :::
116 |
117 | | 
IE / Edge | 
Firefox | 
Chrome | 
Safari | 
iOS Safari |
118 | | --------- | --------- | --------- | --------- | --------- |
119 | | Edge| last version| last version| last version| last version
120 |
121 | > Generated by [browsers-support-badges](http://godban.github.io/browsers-support-badges/)
122 |
123 | ## 更新日志 & 开发计划
124 |
125 | [更新日志](https://github.com/twikoojs/twikoo/releases) & [开发计划](https://github.com/twikoojs/twikoo/projects/2)
126 |
127 |
128 |
129 | ## 特别感谢
130 |
131 | 图标设计:[Maemo Lee](https://www.maemo.cc)
132 |
133 |
134 |
135 | ## 开发
136 |
137 | 如果您想在本地二次开发,可以参考以下命令:
138 |
139 | ``` sh
140 | yarn dev # 开发 (http://localhost:9820/demo.html)
141 | yarn lint # 代码检查
142 | yarn build # 编译 (dist/twikoo.all.min.js)
143 | ```
144 |
145 | 如果您的改动能够帮助到更多人,欢迎提交 Pull Request!
146 |
147 | ## 国际化
148 |
149 | 支持简体中文、繁体中文、English。欢迎[提交翻译 PR](https://github.com/twikoojs/twikoo/edit/main/src/client/utils/i18n/i18n.js)。
150 |
151 | ## 许可
152 |
153 | [](https://app.fossa.com/projects/git%2Bgithub.com%2Fimaegoo%2Ftwikoo?ref=badge_large)
154 |
--------------------------------------------------------------------------------
/docs/link.md:
--------------------------------------------------------------------------------
1 | # 相关文档
2 |
3 | * [在Hexo的Butterfly主题使用Twikoo评论配置及更新教程](https://blog.zhheo.com/p/2e6bbbd0.html) by 张洪 Heo
4 | * [Typecho 到 Twikoo 迁移脚本](https://github.com/Android-KitKat/twikoo-import-tools-typecho) by Android-KitKat
5 | * [Hexo 博客配置 twikoo 评论系统,并调用最新评论](https://www.heson10.com/posts/3217.html) by Heson
6 | * [基于腾讯云,给你的 Icarus 博客配上 Twikoo 评论系统](https://www.anzifan.com/post/icarus_to_candy_2/) by 异次元de机智君💯
7 | * [Twikoo 多个页面共用一个评论区](https://www.imaegoo.com/2021/twikoo-path/) by iMaeGoo
8 | * [集成 Twikoo 与 lightGallery 插件,实现评论图片的点击放大](https://www.imaegoo.com/2021/twikoo-lightgallery/) by iMaeGoo
9 | * [Twikoo 评论数据导出教程](https://www.imaegoo.com/2022/twikoo-data-export/) by iMaeGoo
10 | * [【Hexo博客】Twikoo评论系统的免费部署(云函数采用Vercel方式)](https://blog.meta-code.top/2022/03/16/2022-42/) by 百里飞洋Barry-Flynn
11 | * [React/Next.js 前端应用中接入 Twikoo 前端](https://www.xiaobotalk.com/react-nextjs-%E4%B8%AD%E6%8E%A5%E5%85%A5-twikoo-%E5%89%8D%E7%AB%AF) by XiaoboTalk
12 | * [本站 - 评论模块搭建过程 | Young Kbt blog](https://notes.youngkbt.cn/about/website/comment/) by Young Kbt
13 | * [评论区搭建过程 | 从01开始](https://www.peterjxl.com/Blog/Comment/) by PeterJXL
14 | * [Astro 添加 Twikoo 评论 | 老麦笔记](https://www.iamlm.com/blog/170.Astro%20%E6%B7%BB%E5%8A%A0%20Twikoo%20%E8%AF%84%E8%AE%BA/) by 老麦
15 | * [Re. 从零开始在 Deta Space 部署 Twikoo 评论系统的过程](https://anmeng.asia/cad9d0fd/) by FantasyLand の 暗梦
16 |
--------------------------------------------------------------------------------
/docs/mongodb-atlas.md:
--------------------------------------------------------------------------------
1 | # MongoDB Atlas
2 |
3 | MongoDB Atlas 是 MongoDB Inc 提供的 MongoDB 数据库托管服务。免费账户可以永久使用 500 MiB 的数据库,足够存储 Twikoo 评论使用。
4 |
5 | 1. 申请 [MongoDB AtLas](https://www.mongodb.com/cloud/atlas/register) 账号
6 | 2. 创建免费 MongoDB 数据库,区域推荐选择离 Twikoo 后端(Vercel / Netlify / AWS Lambda / VPS)地理位置较近的数据中心以获得更低的数据库连接延迟。如果不清楚自己的后端在哪个区域,也可选择 `AWS / Oregon (us-west-2)`,该数据中心基建成熟,故障率低,且使用 Oregon 州的清洁能源,较为环保
7 | 3. 在 Database Access 页面点击 Add New Database User 创建数据库用户,Authentication Method 选 Password,在 Password Authentication 下设置数据库用户名和密码,建议点击 Auto Generate 自动生成一个不含特殊符号的强壮密码并妥善保存。点击 Database User Privileges 下方的 Add Built In Role,Select Role 选择 Atlas Admin,最后点击 Add User
8 |
9 | 
10 |
11 | 4. 在 Network Access 页面点击 Add IP Address 添加网络白名单。因为 Vercel / Netlify / Lambda 的出口地址不固定,因此 Access List Entry 输入 `0.0.0.0/0`(允许所有 IP 地址的连接)即可。如果 Twikoo 部署在自己的服务器上,这里可以填入固定 IP 地址。点击 Confirm 保存
12 |
13 | 
14 |
15 | 5. 在 Database 页面点击 Connect,连接方式选择 Drivers,并记录数据库连接字符串,请将连接字符串中的 `:` 修改为刚刚创建的数据库 `用户名:密码`
16 |
17 | 
18 |
19 | 6. (可选)默认的连接字符串没有指定数据库名称,Twikoo 会连接到默认的名为 `test` 的数据库。如果需要在同一个 MongoDB 里运行其他业务或供多个 Twikoo 实例使用,建立加入数据库名称并配置对应的 ACL。
20 |
21 | 连接字符串包含了连接到 MongoDB 数据库的所有信息,一旦泄露会导致评论被任何人添加、修改、删除,并有可能获取你的 SMTP、图床 token 等信息。请妥善记录这一字符串,之后需要填入到 Twikoo 的部署平台里。
22 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twikoo-docs",
3 | "author": "imaegoo (https://github.com/imaegoo)",
4 | "license": "MIT",
5 | "private": true,
6 | "type": "module",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/twikoojs/twikoo.git"
10 | },
11 | "homepage": "https://twikoo.js.org",
12 | "scripts": {
13 | "docs:dev": "vitepress",
14 | "docs:build": "vitepress build"
15 | },
16 | "devDependencies": {
17 | "vitepress": "^1.0.0-rc.29"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/docs/public/twikoo-logo-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/public/twikoo-logo-home.png
--------------------------------------------------------------------------------
/docs/public/twikoo-logo-mini.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/public/twikoo-logo-mini.png
--------------------------------------------------------------------------------
/docs/quick-start.md:
--------------------------------------------------------------------------------
1 | # 快速上手
2 |
3 | Twikoo 分为云函数和前端两部分,若要在您的网站上集成 Twikoo,您需要同时部署云函数和前端,部署时请注意保持二者版本一致。
4 |
5 | * [云函数部署](/backend) 有多种方式,请选择适合自己的部署平台。
6 | * [前端部署](/frontend) 有 2 种方式,如果您的网站主题支持 Twikoo,您只需在配置文件中指定 Twikoo 即可;如果您的网站主题不支持 Twikoo,您需要修改源码手动引入 Twikoo 的 js 文件并初始化。
7 | * 若您已部署旧版本 Twikoo,请参考 [版本更新](/update) 升级云函数和前端版本。
8 |
--------------------------------------------------------------------------------
/docs/static/faq-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/faq-1.png
--------------------------------------------------------------------------------
/docs/static/hugging-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/hugging-1.png
--------------------------------------------------------------------------------
/docs/static/hugging-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/hugging-2.png
--------------------------------------------------------------------------------
/docs/static/hugging-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/hugging-3.png
--------------------------------------------------------------------------------
/docs/static/hugging-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/hugging-4.png
--------------------------------------------------------------------------------
/docs/static/hugging-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/hugging-5.png
--------------------------------------------------------------------------------
/docs/static/hugging-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/hugging-6.png
--------------------------------------------------------------------------------
/docs/static/hugging-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/hugging-7.png
--------------------------------------------------------------------------------
/docs/static/hugging-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/hugging-8.png
--------------------------------------------------------------------------------
/docs/static/hugging-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/hugging-9.png
--------------------------------------------------------------------------------
/docs/static/katex.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/katex.png
--------------------------------------------------------------------------------
/docs/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/logo.png
--------------------------------------------------------------------------------
/docs/static/mongodb-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/mongodb-1.png
--------------------------------------------------------------------------------
/docs/static/mongodb-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/mongodb-2.png
--------------------------------------------------------------------------------
/docs/static/mongodb-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/mongodb-3.png
--------------------------------------------------------------------------------
/docs/static/netlify-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/netlify-1.png
--------------------------------------------------------------------------------
/docs/static/netlify-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/netlify-2.png
--------------------------------------------------------------------------------
/docs/static/netlify-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/netlify-3.png
--------------------------------------------------------------------------------
/docs/static/netlify-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/netlify-4.png
--------------------------------------------------------------------------------
/docs/static/netlify-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/netlify-5.png
--------------------------------------------------------------------------------
/docs/static/readme-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/readme-1.png
--------------------------------------------------------------------------------
/docs/static/readme-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/readme-2.png
--------------------------------------------------------------------------------
/docs/static/readme-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/readme-3.jpg
--------------------------------------------------------------------------------
/docs/static/vercel-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twikoojs/twikoo/e9eb6f276fa6eacc6a2eff1c5a1e6febee9b61f8/docs/static/vercel-1.png
--------------------------------------------------------------------------------
/docs/update.md:
--------------------------------------------------------------------------------
1 | # 版本更新
2 |
3 | 不同部署方式的更新方式也不同,请对号入座。更新部署成功后,请不要忘记同时更新前端的 Twikoo CDN 地址中的 `x.x.x` 数字版本号,使之与云函数版本号相同,然后部署网站。
4 |
5 | ## 针对腾讯云一键部署的更新方式
6 |
7 | 登录[环境-我的应用](https://console.cloud.tencent.com/tcb/apps/index),输入
8 |
9 | * 来源地址:`https://github.com/twikoojs/twikoo/tree/main`
10 | * 部署分支:`main`
11 |
12 | 应用目录无需填写,点击“确定”,部署完成。
13 |
14 | ## 针对腾讯云手动部署的更新方式
15 |
16 | 登录[环境-云函数](https://console.cloud.tencent.com/tcb/scf/index),点击 twikoo,点击函数代码,打开 `package.json` 文件,将 `"twikoo-func": "x.x.x"` 其中的版本号修改为最新版本号,点击“保存并安装依赖”即可。
17 |
18 | ::: tip 提示
19 | 如果您的云函数是 1.0.0 之前的版本,因为 1.0.0 版本修改了部署步骤,请先参考[手动部署](#手动部署),从第 5 步开始,重新创建云函数,再按照此步骤更新。
20 |
21 | 如果升级后出现无法读取评论列表,云函数报错,请在函数编辑页面,删除 `node_modules` 目录(删除需要半分钟左右,请耐心等待删除完成),再点击保存并安装依赖。如果仍然不能解决,请删除并重新创建 Twikoo 云函数。
22 | :::
23 |
24 | ## 针对腾讯云命令行部署的更新方式
25 |
26 | 进入 Twikoo 源码目录,执行以下命令更新现有的云函数
27 |
28 | ``` sh
29 | yarn deploy -e 您的环境id
30 | ```
31 |
32 | ## 针对 Vercel 部署的更新方式
33 |
34 | 1. 进入 [Vercel 仪表板](https://vercel.com/dashboard) - twikoo - Settings - Git
35 | 2. 点击 Connected Git Repository 下方的仓库地址
36 | 3. 打开 package.json,点击编辑
37 | 4. 将 `"twikoo-vercel": "latest"` 其中的 `latest` 修改为最新版本号。点击 Commit changes
38 | 5. 部署会自动触发,可以回到 [Vercel 仪表板](https://vercel.com/dashboard),查看部署状态
39 |
40 | ## 针对 Railway 和 Zeabur 部署的更新方式
41 |
42 | 1. 登录 Github,找到部署时 fork 到自己账号下的名为 twikoo-zeabur 的仓库
43 | 2. 打开 package.json,点击编辑
44 | 3. 将 `"tkserver": "latest"` 其中的 `latest` 修改为最新版本号。点击 Commit changes
45 | 4. 部署会自动触发
46 |
47 | ## 针对 Netlify 部署的更新方式
48 |
49 | 1. 登录 Github,找到部署时 fork 到自己账号下的名为 twikoo-netlify 的仓库
50 | 2. 打开 package.json,点击编辑
51 | 3. 将 `"twikoo-netlify": "latest"` 其中的 `latest` 修改为最新版本号。点击 Commit changes
52 | 4. 部署会自动触发
53 |
54 | ## 针对 Hugging Face 部署的更新方式
55 |
56 | 1. 登录 Hugging Face,找到部署的 Space,点击上方 Settings,往下滚动找到并点击 Factory rebuild
57 |
58 | ## 针对私有部署的更新方式
59 |
60 | 1. 停止旧版本 `kill $(ps -ef | grep tkserver | grep -v 'grep' | awk '{print $2}')`
61 | 2. 拉取新版本 `npm i -g tkserver@latest`
62 | 3. 启动新版本 `nohup tkserver >> tkserver.log 2>&1 &`
63 |
64 | ## 针对私有部署 (Docker) 的更新方式
65 |
66 | 1. 拉取新版本 `docker pull imaegoo/twikoo`
67 | 2. 停止旧版本容器 `docker stop twikoo`
68 | 3. 删除旧版本容器 `docker rm twikoo`
69 | 4. [启动新版本容器](#私有部署-docker)
70 |
71 | ## 自动更新
72 |
73 | 考虑到可用性和安全性问题,Twikoo 没有实现自动更新,也没有计划实现自动更新。如果您希望实现自动更新,可以参考 MHuiG 基于 Github 工作流的 [twikoo-update](https://github.com/MHuiG/twikoo-update) 的实现方式。
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twikoo",
3 | "version": "1.6.44",
4 | "description": "A simple comment system.",
5 | "keywords": [
6 | "twikoojs",
7 | "comment",
8 | "comment-system",
9 | "cloudbase",
10 | "vercel"
11 | ],
12 | "author": "imaegoo (https://github.com/imaegoo)",
13 | "license": "MIT",
14 | "main": "./dist/twikoo.all.min.js",
15 | "publishConfig": {
16 | "access": "public"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/twikoojs/twikoo.git"
21 | },
22 | "homepage": "https://twikoo.js.org",
23 | "scripts": {
24 | "dev": "webpack serve --mode development",
25 | "serve": "webpack serve --mode development",
26 | "build": "cross-env NODE_ENV=production webpack --mode production",
27 | "analyze": "webpack --profile --json > stats.json && webpack-bundle-analyzer stats.json",
28 | "login": "tcb login",
29 | "logout": "tcb logout",
30 | "deploy": "tcb fn deploy twikoo --force",
31 | "lint": "eslint src/** --ignore-path .eslintignore",
32 | "docs:dev": "cd docs && yarn docs:dev",
33 | "docs:build": "cd docs && yarn docs:build"
34 | },
35 | "devDependencies": {
36 | "@babel/cli": "^7.16.0",
37 | "@babel/core": "^7.16.0",
38 | "@babel/plugin-transform-modules-commonjs": "^7.16.0",
39 | "@babel/plugin-transform-runtime": "^7.16.0",
40 | "@babel/preset-env": "^7.16.0",
41 | "@babel/runtime": "^7.16.3",
42 | "@cloudbase/cli": "^1.9.5",
43 | "@cloudbase/js-sdk": "^1.7.1",
44 | "@fortawesome/fontawesome-free": "^5.15.4",
45 | "@webpack-cli/serve": "^1.6.0",
46 | "babel-loader": "^8.2.3",
47 | "blueimp-md5": "^2.19.0",
48 | "copy-webpack-plugin": "^9.0.1",
49 | "cross-env": "^7.0.3",
50 | "css-loader": "^6.5.1",
51 | "element-ui": "^2.15.6",
52 | "eslint": "^8.2.0",
53 | "eslint-config-standard": "^16.0.3",
54 | "eslint-plugin-import": "^2.25.3",
55 | "eslint-plugin-node": "^11.1.0",
56 | "eslint-plugin-promise": "^5.1.1",
57 | "eslint-plugin-standard": "^4.1.0",
58 | "eslint-plugin-vue": "^8.0.3",
59 | "js-sha256": "^0.11.0",
60 | "marked": "^4.0.12",
61 | "mini-css-extract-plugin": "^2.6.1",
62 | "owo": "^1.0.2",
63 | "prismjs": "^1.28.0",
64 | "svg-inline-loader": "^0.8.2",
65 | "terser-webpack-plugin": "^5.2.5",
66 | "vue": "^2.6.14",
67 | "vue-loader": "^15.9.8",
68 | "vue-template-compiler": "^2.6.14",
69 | "webpack": "^5.91.0",
70 | "webpack-bundle-analyzer": "^4.5.0",
71 | "webpack-cli": "^4.9.1",
72 | "webpack-dev-server": "^4.4.0"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/client/lib/marked/README.md:
--------------------------------------------------------------------------------
1 | # Marked v4.0.1 modified by iMaeGoo
2 |
3 | * 支持表情缩写(感谢[深巷里的黑猫](https://blog.csdn.net/qq_22241923/article/details/106900403)提供的思路)
4 |
--------------------------------------------------------------------------------
/src/client/lib/marked/Renderer.js:
--------------------------------------------------------------------------------
1 | import { defaults } from './defaults.js';
2 | import {
3 | cleanUrl,
4 | escape
5 | } from './helpers.js';
6 |
7 | /**
8 | * Renderer
9 | */
10 | export class Renderer {
11 | constructor(options) {
12 | this.options = options || defaults;
13 | }
14 |
15 | owo(text) {
16 | const odata = this.options.odata;
17 | if (odata && odata[text]) {
18 | return '
';
23 | } else {
24 | return ':' + text + ':';
25 | }
26 | }
27 |
28 | code(code, infostring, escaped) {
29 | const lang = (infostring || '').match(/\S*/)[0];
30 | if (this.options.highlight) {
31 | const out = this.options.highlight(code, lang);
32 | if (out != null && out !== code) {
33 | escaped = true;
34 | code = out;
35 | }
36 | }
37 |
38 | code = code.replace(/\n$/, '') + '\n';
39 |
40 | if (!lang) {
41 | return ''
42 | + (escaped ? code : escape(code, true))
43 | + '
\n';
44 | }
45 |
46 | return ''
50 | + (escaped ? code : escape(code, true))
51 | + '
\n';
52 | }
53 |
54 | blockquote(quote) {
55 | return '\n' + quote + '
\n';
56 | }
57 |
58 | html(html) {
59 | return html;
60 | }
61 |
62 | heading(text, level, raw, slugger) {
63 | if (this.options.headerIds) {
64 | return '\n';
74 | }
75 | // ignore IDs
76 | return '' + text + '\n';
77 | }
78 |
79 | hr() {
80 | return this.options.xhtml ? '
\n' : '
\n';
81 | }
82 |
83 | list(body, ordered, start) {
84 | const type = ordered ? 'ol' : 'ul',
85 | startatt = (ordered && start !== 1) ? (' start="' + start + '"') : '';
86 | return '<' + type + startatt + '>\n' + body + '' + type + '>\n';
87 | }
88 |
89 | listitem(text) {
90 | return '' + text + '\n';
91 | }
92 |
93 | checkbox(checked) {
94 | return ' ';
99 | }
100 |
101 | paragraph(text) {
102 | return '' + text + '
\n';
103 | }
104 |
105 | table(header, body) {
106 | if (body) body = '' + body + '';
107 |
108 | return '\n'
109 | + '\n'
110 | + header
111 | + '\n'
112 | + body
113 | + '
\n';
114 | }
115 |
116 | tablerow(content) {
117 | return '\n' + content + '
\n';
118 | }
119 |
120 | tablecell(content, flags) {
121 | const type = flags.header ? 'th' : 'td';
122 | const tag = flags.align
123 | ? '<' + type + ' align="' + flags.align + '">'
124 | : '<' + type + '>';
125 | return tag + content + '' + type + '>\n';
126 | }
127 |
128 | // span level renderer
129 | strong(text) {
130 | return '' + text + '';
131 | }
132 |
133 | em(text) {
134 | return '' + text + '';
135 | }
136 |
137 | codespan(text) {
138 | return '' + text + '
';
139 | }
140 |
141 | br() {
142 | return this.options.xhtml ? '
' : '
';
143 | }
144 |
145 | del(text) {
146 | return '' + text + '';
147 | }
148 |
149 | link(href, title, text) {
150 | href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
151 | if (href === null) {
152 | return text;
153 | }
154 | let out = '' + text + '';
159 | return out;
160 | }
161 |
162 | image(href, title, text) {
163 | href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
164 | if (href === null) {
165 | return text;
166 | }
167 |
168 | let out = '
' : '>';
173 | return out;
174 | }
175 |
176 | text(text) {
177 | return text;
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/src/client/lib/marked/Slugger.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Slugger generates header id
3 | */
4 | export class Slugger {
5 | constructor() {
6 | this.seen = {};
7 | }
8 |
9 | serialize(value) {
10 | return value
11 | .toLowerCase()
12 | .trim()
13 | // remove html tags
14 | .replace(/<[!\/a-z].*?>/ig, '')
15 | // remove unwanted chars
16 | .replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g, '')
17 | .replace(/\s/g, '-');
18 | }
19 |
20 | /**
21 | * Finds the next safe (unique) slug to use
22 | */
23 | getNextSafeSlug(originalSlug, isDryRun) {
24 | let slug = originalSlug;
25 | let occurenceAccumulator = 0;
26 | if (this.seen.hasOwnProperty(slug)) {
27 | occurenceAccumulator = this.seen[originalSlug];
28 | do {
29 | occurenceAccumulator++;
30 | slug = originalSlug + '-' + occurenceAccumulator;
31 | } while (this.seen.hasOwnProperty(slug));
32 | }
33 | if (!isDryRun) {
34 | this.seen[originalSlug] = occurenceAccumulator;
35 | this.seen[slug] = 0;
36 | }
37 | return slug;
38 | }
39 |
40 | /**
41 | * Convert string to unique id
42 | * @param {object} options
43 | * @param {boolean} options.dryrun Generates the next unique slug without updating the internal accumulator.
44 | */
45 | slug(value, options = {}) {
46 | const slug = this.serialize(value);
47 | return this.getNextSafeSlug(slug, options.dryrun);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/client/lib/marked/TextRenderer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * TextRenderer
3 | * returns only the textual part of the token
4 | */
5 | export class TextRenderer {
6 | // no need for block level renderers
7 | strong(text) {
8 | return text;
9 | }
10 |
11 | em(text) {
12 | return text;
13 | }
14 |
15 | codespan(text) {
16 | return text;
17 | }
18 |
19 | del(text) {
20 | return text;
21 | }
22 |
23 | html(text) {
24 | return text;
25 | }
26 |
27 | text(text) {
28 | return text;
29 | }
30 |
31 | link(href, title, text) {
32 | return '' + text;
33 | }
34 |
35 | image(href, title, text) {
36 | return '' + text;
37 | }
38 |
39 | br() {
40 | return '';
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/client/lib/marked/defaults.js:
--------------------------------------------------------------------------------
1 | export function getDefaults() {
2 | return {
3 | baseUrl: null,
4 | breaks: false,
5 | extensions: null,
6 | gfm: true,
7 | headerIds: true,
8 | headerPrefix: '',
9 | highlight: null,
10 | langPrefix: 'language-',
11 | mangle: true,
12 | pedantic: false,
13 | renderer: null,
14 | sanitize: false,
15 | sanitizer: null,
16 | silent: false,
17 | smartLists: false,
18 | smartypants: false,
19 | tokenizer: null,
20 | walkTokens: null,
21 | xhtml: false
22 | };
23 | }
24 |
25 | export let defaults = getDefaults();
26 |
27 | export function changeDefaults(newDefaults) {
28 | defaults = newDefaults;
29 | }
30 |
--------------------------------------------------------------------------------
/src/client/lib/marked/helpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Helpers
3 | */
4 | const escapeTest = /[&<>"']/;
5 | const escapeReplace = /[&<>"']/g;
6 | const escapeTestNoEncode = /[<>"']|&(?!#?\w+;)/;
7 | const escapeReplaceNoEncode = /[<>"']|&(?!#?\w+;)/g;
8 | const escapeReplacements = {
9 | '&': '&',
10 | '<': '<',
11 | '>': '>',
12 | '"': '"',
13 | "'": '''
14 | };
15 | const getEscapeReplacement = (ch) => escapeReplacements[ch];
16 | export function escape(html, encode) {
17 | if (encode) {
18 | if (escapeTest.test(html)) {
19 | return html.replace(escapeReplace, getEscapeReplacement);
20 | }
21 | } else {
22 | if (escapeTestNoEncode.test(html)) {
23 | return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
24 | }
25 | }
26 |
27 | return html;
28 | }
29 |
30 | const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;
31 |
32 | export function unescape(html) {
33 | // explicitly match decimal, hex, and named HTML entities
34 | return html.replace(unescapeTest, (_, n) => {
35 | n = n.toLowerCase();
36 | if (n === 'colon') return ':';
37 | if (n.charAt(0) === '#') {
38 | return n.charAt(1) === 'x'
39 | ? String.fromCharCode(parseInt(n.substring(2), 16))
40 | : String.fromCharCode(+n.substring(1));
41 | }
42 | return '';
43 | });
44 | }
45 |
46 | const caret = /(^|[^\[])\^/g;
47 | export function edit(regex, opt) {
48 | regex = regex.source || regex;
49 | opt = opt || '';
50 | const obj = {
51 | replace: (name, val) => {
52 | val = val.source || val;
53 | val = val.replace(caret, '$1');
54 | regex = regex.replace(name, val);
55 | return obj;
56 | },
57 | getRegex: () => {
58 | return new RegExp(regex, opt);
59 | }
60 | };
61 | return obj;
62 | }
63 |
64 | const nonWordAndColonTest = /[^\w:]/g;
65 | const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
66 | export function cleanUrl(sanitize, base, href) {
67 | if (sanitize) {
68 | let prot;
69 | try {
70 | prot = decodeURIComponent(unescape(href))
71 | .replace(nonWordAndColonTest, '')
72 | .toLowerCase();
73 | } catch (e) {
74 | return null;
75 | }
76 | if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
77 | return null;
78 | }
79 | }
80 | if (base && !originIndependentUrl.test(href)) {
81 | href = resolveUrl(base, href);
82 | }
83 | try {
84 | href = encodeURI(href).replace(/%25/g, '%');
85 | } catch (e) {
86 | return null;
87 | }
88 | return href;
89 | }
90 |
91 | const baseUrls = {};
92 | const justDomain = /^[^:]+:\/*[^/]*$/;
93 | const protocol = /^([^:]+:)[\s\S]*$/;
94 | const domain = /^([^:]+:\/*[^/]*)[\s\S]*$/;
95 |
96 | export function resolveUrl(base, href) {
97 | if (!baseUrls[' ' + base]) {
98 | // we can ignore everything in base after the last slash of its path component,
99 | // but we might need to add _that_
100 | // https://tools.ietf.org/html/rfc3986#section-3
101 | if (justDomain.test(base)) {
102 | baseUrls[' ' + base] = base + '/';
103 | } else {
104 | baseUrls[' ' + base] = rtrim(base, '/', true);
105 | }
106 | }
107 | base = baseUrls[' ' + base];
108 | const relativeBase = base.indexOf(':') === -1;
109 |
110 | if (href.substring(0, 2) === '//') {
111 | if (relativeBase) {
112 | return href;
113 | }
114 | return base.replace(protocol, '$1') + href;
115 | } else if (href.charAt(0) === '/') {
116 | if (relativeBase) {
117 | return href;
118 | }
119 | return base.replace(domain, '$1') + href;
120 | } else {
121 | return base + href;
122 | }
123 | }
124 |
125 | export const noopTest = { exec: function noopTest() {} };
126 |
127 | export function merge(obj) {
128 | let i = 1,
129 | target,
130 | key;
131 |
132 | for (; i < arguments.length; i++) {
133 | target = arguments[i];
134 | for (key in target) {
135 | if (Object.prototype.hasOwnProperty.call(target, key)) {
136 | obj[key] = target[key];
137 | }
138 | }
139 | }
140 |
141 | return obj;
142 | }
143 |
144 | export function splitCells(tableRow, count) {
145 | // ensure that every cell-delimiting pipe has a space
146 | // before it to distinguish it from an escaped pipe
147 | const row = tableRow.replace(/\|/g, (match, offset, str) => {
148 | let escaped = false,
149 | curr = offset;
150 | while (--curr >= 0 && str[curr] === '\\') escaped = !escaped;
151 | if (escaped) {
152 | // odd number of slashes means | is escaped
153 | // so we leave it alone
154 | return '|';
155 | } else {
156 | // add space before unescaped |
157 | return ' |';
158 | }
159 | }),
160 | cells = row.split(/ \|/);
161 | let i = 0;
162 |
163 | // First/last cell in a row cannot be empty if it has no leading/trailing pipe
164 | if (!cells[0].trim()) { cells.shift(); }
165 | if (cells.length > 0 && !cells[cells.length - 1].trim()) { cells.pop(); }
166 |
167 | if (cells.length > count) {
168 | cells.splice(count);
169 | } else {
170 | while (cells.length < count) cells.push('');
171 | }
172 |
173 | for (; i < cells.length; i++) {
174 | // leading or trailing whitespace is ignored per the gfm spec
175 | cells[i] = cells[i].trim().replace(/\\\|/g, '|');
176 | }
177 | return cells;
178 | }
179 |
180 | // Remove trailing 'c's. Equivalent to str.replace(/c*$/, '').
181 | // /c*$/ is vulnerable to REDOS.
182 | // invert: Remove suffix of non-c chars instead. Default falsey.
183 | export function rtrim(str, c, invert) {
184 | const l = str.length;
185 | if (l === 0) {
186 | return '';
187 | }
188 |
189 | // Length of suffix matching the invert condition.
190 | let suffLen = 0;
191 |
192 | // Step left until we fail to match the invert condition.
193 | while (suffLen < l) {
194 | const currChar = str.charAt(l - suffLen - 1);
195 | if (currChar === c && !invert) {
196 | suffLen++;
197 | } else if (currChar !== c && invert) {
198 | suffLen++;
199 | } else {
200 | break;
201 | }
202 | }
203 |
204 | return str.substr(0, l - suffLen);
205 | }
206 |
207 | export function findClosingBracket(str, b) {
208 | if (str.indexOf(b[1]) === -1) {
209 | return -1;
210 | }
211 | const l = str.length;
212 | let level = 0,
213 | i = 0;
214 | for (; i < l; i++) {
215 | if (str[i] === '\\') {
216 | i++;
217 | } else if (str[i] === b[0]) {
218 | level++;
219 | } else if (str[i] === b[1]) {
220 | level--;
221 | if (level < 0) {
222 | return i;
223 | }
224 | }
225 | }
226 | return -1;
227 | }
228 |
229 | export function checkSanitizeDeprecation(opt) {
230 | if (opt && opt.sanitize && !opt.silent) {
231 | console.warn('marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options');
232 | }
233 | }
234 |
235 | // copied from https://stackoverflow.com/a/5450113/806777
236 | export function repeatString(pattern, count) {
237 | if (count < 1) {
238 | return '';
239 | }
240 | let result = '';
241 | while (count > 1) {
242 | if (count & 1) {
243 | result += pattern;
244 | }
245 | count >>= 1;
246 | pattern += pattern;
247 | }
248 | return result + pattern;
249 | }
250 |
--------------------------------------------------------------------------------
/src/client/lib/owo.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * OwO v1.0.2
3 | * Source: https://github.com/DIYgod/OwO/blob/master/dist/OwO.min.css
4 | * Author: DIYgod
5 | * Modified by: iMaeGoo
6 | * Released under the MIT License.
7 | */
8 |
9 | .OwO {
10 | -webkit-user-select: none;
11 | -moz-user-select: none;
12 | -ms-user-select: none;
13 | user-select: none;
14 | }
15 |
16 | .OwO.OwO-open .OwO-body {
17 | display: block;
18 | }
19 |
20 | .OwO .OwO-logo {
21 | width: 1.125em;
22 | display: flex;
23 | }
24 |
25 | .OwO .OwO-body {
26 | display: none;
27 | position: absolute;
28 | left: 0;
29 | right: 0;
30 | max-width: 500px;
31 | color: #4a4a4a;
32 | background-color: #ffffff;
33 | border: 1px solid rgba(144,147,153,0.31);
34 | top: 2em;
35 | border-radius: 0 4px 4px;
36 | z-index: 1000;
37 | }
38 |
39 | .night .OwO .OwO-body,
40 | .darkmode .OwO .OwO-body,
41 | .DarkMode .OwO .OwO-body,
42 | [data-theme="dark"] .OwO .OwO-body,
43 | [data-user-color-scheme="dark"] .OwO .OwO-body {
44 | color: #ffffff;
45 | background-color: #4a4a4a;
46 | }
47 |
48 | .OwO .OwO-body .OwO-items {
49 | -webkit-user-select: none;
50 | -moz-user-select: none;
51 | -ms-user-select: none;
52 | user-select: none;
53 | display: none;
54 | padding: 10px;
55 | padding-right: 0;
56 | margin: 0;
57 | overflow: auto;
58 | font-size: 0;
59 | }
60 |
61 | .OwO .OwO-body .OwO-items .OwO-item {
62 | list-style-type: none;
63 | padding: 5px 10px;
64 | border-radius: 5px;
65 | display: inline-block;
66 | font-size: 12px;
67 | line-height: 14px;
68 | cursor: pointer;
69 | -webkit-transition: .3s;
70 | transition: .3s;
71 | text-align: center;
72 | }
73 |
74 | .OwO .OwO-body .OwO-items .OwO-item:hover {
75 | background-color: rgba(144,147,153,0.13);
76 | box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
77 | }
78 |
79 | .OwO .OwO-body .OwO-items-emoji .OwO-item {
80 | font-size: 20px;
81 | line-height: 19px;
82 | }
83 |
84 | .OwO .OwO-body .OwO-items-image .OwO-item {
85 | width: 14%;
86 | box-sizing: border-box;
87 | }
88 |
89 | @media screen and (max-width: 600px) {
90 | #twikoo .OwO-items > .OwO-item {
91 | width: 16%;
92 | }
93 | }
94 |
95 | @media screen and (max-width: 460px) {
96 | #twikoo .OwO-items > .OwO-item {
97 | width: 20%;
98 | }
99 | }
100 |
101 | @media screen and (max-width: 400px) {
102 | #twikoo .OwO-items > .OwO-item {
103 | width: 25%;
104 | }
105 | }
106 |
107 | @media screen and (max-width: 330px) {
108 | #twikoo .OwO-items > .OwO-item {
109 | width: 33%;
110 | }
111 | }
112 |
113 |
114 | .OwO .OwO-body .OwO-items-image .OwO-item img {
115 | max-width: 100%;
116 | }
117 |
118 | .OwO .OwO-body .OwO-items-show {
119 | display: block;
120 | }
121 |
122 | .OwO .OwO-body .OwO-bar {
123 | width: 100%;
124 | border-top: 1px solid rgba(144,147,153,0.31);
125 | border-radius: 0 0 4px 4px;
126 | }
127 |
128 | .OwO .OwO-body .OwO-bar .OwO-packages {
129 | margin: 0;
130 | padding: 0;
131 | font-size: 0;
132 | }
133 |
134 | .OwO .OwO-body .OwO-bar .OwO-packages li {
135 | list-style-type: none;
136 | display: inline-block;
137 | line-height: 30px;
138 | font-size: 14px;
139 | padding: 0 10px;
140 | cursor: pointer;
141 | margin-right: 3px;
142 | }
143 |
144 | .OwO .OwO-body .OwO-bar .OwO-packages li:nth-child(1) {
145 | border-radius: 0 0 0 3px;
146 | }
147 |
148 | .OwO .OwO-body .OwO-bar .OwO-packages li:hover {
149 | background-color: rgba(144,147,153,0.13);
150 | }
151 |
152 | .OwO .OwO-body .OwO-bar .OwO-packages .OwO-package-active {
153 | background-color: rgba(144,147,153,0.13);
154 | -webkit-transition: .3s;
155 | transition: .3s;
156 | }
157 |
--------------------------------------------------------------------------------
/src/client/lib/owo.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * OwO v1.0.2
3 | * Source: https://github.com/DIYgod/OwO/blob/master/src/OwO.js
4 | * Author: DIYgod
5 | * Modified by: iMaeGoo
6 | * Released under the MIT License.
7 | */
8 |
9 | export default class OwO {
10 | constructor (option) {
11 | const defaultOption = {
12 | logo: 'OwO表情',
13 | container: document.getElementsByClassName('OwO')[0],
14 | target: document.getElementsByTagName('textarea')[0],
15 | position: 'down',
16 | maxHeight: '250px',
17 | odata: {}
18 | }
19 | for (const defaultKey in defaultOption) {
20 | if (defaultOption[defaultKey] && !option[defaultKey]) {
21 | option[defaultKey] = defaultOption[defaultKey]
22 | }
23 | }
24 | this.container = option.container
25 | this.target = option.target
26 | if (option.position === 'up') {
27 | this.container.classList.add('OwO-up')
28 | }
29 |
30 | this.odata = option.odata
31 | setTimeout(() => { this.init(option) })
32 | }
33 |
34 | init (option) {
35 | this.area = option.target
36 | this.packages = Object.keys(this.odata)
37 |
38 | // fill in HTML
39 | let html = `${option.logo}
` +
40 | ''
41 |
42 | for (let i = 0; i < this.packages.length; i++) {
43 | html += `
`
44 |
45 | const opackage = this.odata[this.packages[i]].container
46 | for (let i = 0; i < opackage.length; i++) {
47 | const icon = opackage[i].icon.replace('
${icon}`
49 | }
50 |
51 | html += '
'
52 | }
53 |
54 | html += '
' +
55 | '
'
56 |
57 | for (let i = 0; i < this.packages.length; i++) {
58 | html += `- ${this.packages[i]}
`
59 | }
60 |
61 | html += '
'
62 | this.container.innerHTML = html
63 |
64 | // bind event
65 | this.logo = this.container.getElementsByClassName('OwO-logo')[0]
66 | this.logo.addEventListener('click', () => {
67 | this.toggle()
68 | })
69 |
70 | this.container.getElementsByClassName('OwO-body')[0].addEventListener('click', (e) => {
71 | let target = null
72 | if (e.target.classList.contains('OwO-item')) {
73 | target = e.target
74 | } else if (e.target.parentNode.classList.contains('OwO-item')) {
75 | target = e.target.parentNode
76 | }
77 | if (target) {
78 | const cursorPos = this.area.selectionEnd
79 | const areaValue = this.area.value
80 | let innerHTML = target.innerHTML
81 | if (innerHTML.indexOf('
{
104 | this.packagesEle.children[i].addEventListener('click', () => {
105 | this.tab(index)
106 | })
107 | })(i)
108 | }
109 |
110 | this.tab(0)
111 | }
112 |
113 | toggle () {
114 | if (this.container.classList.contains('OwO-open')) {
115 | this.container.classList.remove('OwO-open')
116 | } else {
117 | this.container.classList.add('OwO-open')
118 | }
119 | }
120 |
121 | tab (index) {
122 | const itemsShow = this.container.getElementsByClassName('OwO-items-show')[0]
123 | if (itemsShow) {
124 | itemsShow.classList.remove('OwO-items-show')
125 | }
126 | this.container.getElementsByClassName('OwO-items')[index].classList.add('OwO-items-show')
127 |
128 | const packageActive = this.container.getElementsByClassName('OwO-package-active')[0]
129 | if (packageActive) {
130 | packageActive.classList.remove('OwO-package-active')
131 | }
132 | this.packagesEle.getElementsByTagName('li')[index].classList.add('OwO-package-active')
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/client/main.all.js:
--------------------------------------------------------------------------------
1 | import { version } from './version'
2 | import { install } from './utils/tcb'
3 | import { render } from './view'
4 | import { setLanguage, isUrl, getCommentsCountApi, getRecentCommentsApi } from './utils'
5 | import cloudbase from '@cloudbase/js-sdk/app'
6 | import '@cloudbase/js-sdk/auth'
7 | import '@cloudbase/js-sdk/functions'
8 | import '@cloudbase/js-sdk/storage'
9 |
10 | async function initTcb (options) {
11 | return await install(cloudbase, options)
12 | }
13 |
14 | async function init (options = {}) {
15 | const tcb = isUrl(options.envId) ? null : await initTcb(options)
16 | setLanguage(options)
17 | render(tcb, options)
18 | }
19 |
20 | async function getCommentsCount (options = {}) {
21 | const tcb = isUrl(options.envId) ? null : await initTcb(options)
22 | return await getCommentsCountApi(tcb, options)
23 | }
24 |
25 | async function getRecentComments (options = {}) {
26 | const tcb = isUrl(options.envId) ? null : await initTcb(options)
27 | return await getRecentCommentsApi(tcb, options)
28 | }
29 |
30 | export default init
31 | export {
32 | version,
33 | init,
34 | getCommentsCount,
35 | getRecentComments
36 | }
37 |
--------------------------------------------------------------------------------
/src/client/main.js:
--------------------------------------------------------------------------------
1 | import { version } from './version'
2 | import { install } from './utils/tcb'
3 | import { render } from './view'
4 | import { setLanguage, logger, isUrl, getCommentsCountApi, getRecentCommentsApi } from './utils'
5 |
6 | async function initTcb (options) {
7 | if (typeof cloudbase === 'undefined') {
8 | logger.error('Please import cloudbase firstly:\n')
9 | return null
10 | }
11 | /* eslint-disable-next-line no-undef */
12 | return await install(cloudbase, options)
13 | }
14 |
15 | async function init (options = {}) {
16 | const tcb = isUrl(options.envId) ? null : await initTcb(options)
17 | setLanguage(options)
18 | render(tcb, options)
19 | }
20 |
21 | async function getCommentsCount (options = {}) {
22 | const tcb = isUrl(options.envId) ? null : await initTcb(options)
23 | return await getCommentsCountApi(tcb, options)
24 | }
25 |
26 | async function getRecentComments (options = {}) {
27 | const tcb = isUrl(options.envId) ? null : await initTcb(options)
28 | return await getRecentCommentsApi(tcb, options)
29 | }
30 |
31 | export default init
32 | export {
33 | version,
34 | init,
35 | getCommentsCount,
36 | getRecentComments
37 | }
38 |
--------------------------------------------------------------------------------
/src/client/utils/api.js:
--------------------------------------------------------------------------------
1 | import { app } from '../view'
2 |
3 | const isUrl = (s) => {
4 | return /^http(s)?:\/\//.test(s)
5 | }
6 |
7 | const call = async (tcb, event, data = {}) => {
8 | const _tcb = tcb || (app ? app.$tcb : null)
9 | const _envId = data.envId || app.$twikoo.envId
10 | const _funcName = data.funcName || app?.$twikoo.funcName || 'twikoo'
11 | if (_tcb) {
12 | try {
13 | return await _tcb.app.callFunction({
14 | name: _funcName,
15 | data: { event, ...data }
16 | })
17 | } catch (e) {
18 | // 向下兼容 0.1.x 版本云函数
19 | let oldFuncName
20 | switch (event) {
21 | case 'COMMENT_LIKE':
22 | oldFuncName = 'comment-like'
23 | break
24 | case 'COMMENT_GET':
25 | oldFuncName = 'comment-get'
26 | break
27 | case 'COMMENT_SUBMIT':
28 | oldFuncName = 'comment-submit'
29 | break
30 | case 'COUNTER_GET':
31 | oldFuncName = 'counter-get'
32 | break
33 | }
34 | if (oldFuncName) {
35 | return await _tcb.app.callFunction({
36 | name: oldFuncName,
37 | data: data
38 | })
39 | } else {
40 | throw new Error('请升级 Twikoo 云函数版本再试,如果仍无法解决,请删除并重新创建 Twikoo 云函数 - https://twikoo.js.org')
41 | }
42 | }
43 | } else if (isUrl(_envId)) {
44 | return await new Promise((resolve, reject) => {
45 | try {
46 | const accessToken = localStorage.getItem('twikoo-access-token')
47 | const xhr = new XMLHttpRequest()
48 | xhr.onreadystatechange = () => {
49 | if (xhr.readyState === 4) {
50 | if (xhr.status === 200) {
51 | const result = JSON.parse(xhr.responseText)
52 | if (result.accessToken) {
53 | localStorage.setItem('twikoo-access-token', result.accessToken)
54 | }
55 | resolve({ result })
56 | } else {
57 | reject(xhr.status)
58 | }
59 | }
60 | }
61 | xhr.open('POST', _envId)
62 | xhr.setRequestHeader('Content-Type', 'application/json')
63 | xhr.send(JSON.stringify({ event, accessToken, ...data }))
64 | } catch (e) {
65 | reject(e)
66 | }
67 | })
68 | } else {
69 | throw new Error('缺少 envId 配置 - https://twikoo.js.org')
70 | }
71 | }
72 |
73 | export {
74 | isUrl,
75 | call
76 | }
77 |
--------------------------------------------------------------------------------
/src/client/utils/avatar.js:
--------------------------------------------------------------------------------
1 | function normalizeMail (mail) {
2 | return String(mail).trim().toLowerCase()
3 | }
4 |
5 | function isQQ (mail) {
6 | return /^[1-9][0-9]{4,10}$/.test(mail) ||
7 | /^[1-9][0-9]{4,10}@qq.com$/i.test(mail)
8 | }
9 |
10 | function getQQAvatar (qq) {
11 | const qqNum = qq.replace(/@qq.com/ig, '')
12 | return `https://thirdqq.qlogo.cn/g?b=sdk&nk=${qqNum}&s=140`
13 | }
14 |
15 | export {
16 | normalizeMail,
17 | isQQ,
18 | getQQAvatar
19 | }
20 |
--------------------------------------------------------------------------------
/src/client/utils/emotion.js:
--------------------------------------------------------------------------------
1 | import { logger } from '.'
2 |
3 | function initOwoEmotion (api) {
4 | return new Promise((resolve) => {
5 | const xhr = new XMLHttpRequest()
6 | xhr.onreadystatechange = () => {
7 | if (xhr.readyState === 4) {
8 | if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
9 | const odata = formatOdata(JSON.parse(xhr.responseText))
10 | resolve(odata)
11 | } else {
12 | logger.warn('OwO data request was unsuccessful: ' + xhr.status)
13 | }
14 | }
15 | }
16 | xhr.open('get', api, true)
17 | xhr.send(null)
18 | })
19 | }
20 |
21 | async function initOwoEmotions (apis) {
22 | const odata = {}
23 | const odatas = await Promise.all(apis.split(',').map((api) => initOwoEmotion(api.trim())))
24 | Object.assign(odata, ...odatas)
25 | return odata
26 | }
27 |
28 | // 格式化不规范的 OwO 数据格式
29 | function formatOdata (odata) {
30 | try {
31 | Object.values(odata).forEach(item => {
32 | if (item.type === 'image') {
33 | for (const image of item.container) {
34 | if (!image.text) {
35 | // 缺少 text 的,取 img 文件名作为 text
36 | image.text = getFilename(getImgSrc(image.icon))
37 | }
38 | }
39 | }
40 | })
41 | return odata
42 | } catch (e) {
43 | logger.warn('OwO data is bad: ', e)
44 | }
45 | }
46 |
47 | const template = document.createElement('template')
48 | function getImgSrc (html) {
49 | try {
50 | template.innerHTML = html
51 | return template.content.childNodes[0].src
52 | } catch (e) {
53 | return ''
54 | }
55 | }
56 |
57 | function getFilename (url) {
58 | return url.split('#').shift().split('?').shift().split('/').pop()
59 | }
60 |
61 | function initMarkedOwo (odata) {
62 | if (odata && Object.values(odata)) {
63 | const imgs = {}
64 | Object
65 | .values(odata)
66 | .forEach(item => {
67 | item.container.forEach(img => {
68 | const imgSrc = getImgSrc(img.icon)
69 | if (imgSrc) {
70 | imgs[img.text] = imgSrc
71 | }
72 | })
73 | })
74 | return imgs
75 | }
76 | }
77 |
78 | export {
79 | initOwoEmotions,
80 | initMarkedOwo
81 | }
82 |
--------------------------------------------------------------------------------
/src/client/utils/highlight.js:
--------------------------------------------------------------------------------
1 | import { app } from '../view'
2 |
3 | const PRISM_CDN = 'https://cdn.jsdelivr.net/npm/prismjs@1.28.0'
4 | let Prism
5 | let cssEl
6 |
7 | const renderCode = (el, theme, plugins) => {
8 | const prismCdn = (app && app.$twikoo.prismCdn) ? app.$twikoo.prismCdn : PRISM_CDN
9 | window.Prism = window.Prism || {}
10 | window.Prism.manual = true
11 | if (!Prism) {
12 | Prism = require('prismjs')
13 | require('prismjs/plugins/autoloader/prism-autoloader')
14 | Prism.plugins.autoloader.languages_path = `${prismCdn}/components/`
15 | if (plugins) {
16 | require('prismjs/plugins/toolbar/prism-toolbar')
17 | plugins.split(',').map(item => { return item.trim() }).forEach(p => {
18 | if (p === 'showLanguage') {
19 | require('prismjs/plugins/show-language/prism-show-language')
20 | } else if (p === 'copyButton') {
21 | require('prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard')
22 | }
23 | })
24 | }
25 | }
26 | loadCss(theme, prismCdn)
27 | Prism.highlightAllUnder(el)
28 | }
29 |
30 | const loadCss = (theme, prismCdn) => {
31 | const twikooEl = document.getElementById('twikoo')
32 | if ((cssEl && twikooEl.contains(cssEl)) || !theme || theme === 'none') return
33 | cssEl = document.createElement('link')
34 | if (theme === 'default') {
35 | cssEl.href = `${prismCdn}/themes/prism.min.css`
36 | } else {
37 | cssEl.href = `${prismCdn}/themes/prism-${theme}.min.css`
38 | }
39 | cssEl.rel = 'stylesheet'
40 | cssEl.type = 'text/css'
41 | twikooEl.appendChild(cssEl)
42 | }
43 |
44 | export default renderCode
45 |
--------------------------------------------------------------------------------
/src/client/utils/i18n/index.js:
--------------------------------------------------------------------------------
1 | import i18n from './i18n'
2 |
3 | // ISO Language Code Table http://www.lingoes.net/en/translator/langcode.htm
4 | // RSS Language Code Table https://www.rssboard.org/rss-language-codes
5 |
6 | const langs = {
7 | zh: 0,
8 | 'zh-cn': 0,
9 | 'zh-hk': 1,
10 | 'zh-tw': 2,
11 | 'en-us': 3,
12 | 'en-gb': 3,
13 | en: 3,
14 | uz: 4,
15 | 'uz-uz': 4,
16 | ja: 5,
17 | 'ja-jp': 5,
18 | ko: 6,
19 | 'ko-kr': 6
20 | }
21 |
22 | const defaultLanguage = 'zh-cn'
23 | let twikooLangOption = ''
24 |
25 | const setLanguage = (options = {}) => {
26 | if (options.lang && options.lang.toLowerCase() in langs) {
27 | twikooLangOption = options.lang
28 | }
29 | }
30 |
31 | const translate = (key, language) => {
32 | // 优先级: translate 入参 > twikoo.init 入参 > 浏览器语言设置 > 默认语言
33 | const lang = (language || twikooLangOption || navigator.language).toLowerCase()
34 | let value
35 | if (lang && langs[lang]) {
36 | value = i18n[key][langs[lang]]
37 | } else {
38 | value = i18n[key][langs[defaultLanguage]]
39 | }
40 | return value || ''
41 | }
42 |
43 | export default translate
44 | export {
45 | setLanguage
46 | }
47 |
--------------------------------------------------------------------------------
/src/client/utils/index.js:
--------------------------------------------------------------------------------
1 | import t, { setLanguage } from './i18n'
2 | import timeago from './timeago'
3 | import marked from './marked'
4 | import renderCode from './highlight'
5 | import { isUrl, call } from './api'
6 | import { normalizeMail, isQQ, getQQAvatar } from './avatar'
7 | import { initOwoEmotions, initMarkedOwo } from './emotion'
8 |
9 | const isNotSet = (option) => {
10 | return option === undefined || option === null || option === ''
11 | }
12 |
13 | const logger = {
14 | log: (message, e) => {
15 | console.log(`Twikoo: ${message}`, e)
16 | },
17 | info: (message, e) => {
18 | console.info(`Twikoo: ${message}`, e)
19 | },
20 | warn: (message, e) => {
21 | console.warn(`Twikoo: ${message}`, e)
22 | },
23 | error: (message, e) => {
24 | console.error(`Twikoo: ${message}`, e)
25 | }
26 | }
27 |
28 | const timestamp = (date = new Date()) => {
29 | return date.getTime()
30 | }
31 |
32 | const convertLink = (link) => {
33 | if (!link) return ''
34 | if (link.substring(0, 4) !== 'http') return `http://${link}`
35 | return link
36 | }
37 |
38 | // 云函数版本
39 | let twikooFuncVer
40 | const getFuncVer = async (tcb) => {
41 | if (!twikooFuncVer) {
42 | twikooFuncVer = await call(tcb, 'GET_FUNC_VERSION')
43 | }
44 | return twikooFuncVer
45 | }
46 |
47 | const getCommentsCountApi = async (tcb, options) => {
48 | if (!(options.urls instanceof Array)) {
49 | throw new Error('urls 参数有误')
50 | }
51 | if (options.urls.length === 0) {
52 | return []
53 | }
54 | const result = await call(tcb, 'GET_COMMENTS_COUNT', options)
55 | return result.result.data
56 | }
57 |
58 | const getRecentCommentsApi = async (tcb, options) => {
59 | const result = await call(tcb, 'GET_RECENT_COMMENTS', options)
60 | // 封装相对评论时间
61 | for (const comment of result.result.data) {
62 | comment.relativeTime = timeago(comment.created)
63 | }
64 | return result.result.data
65 | }
66 |
67 | /**
68 | * 替换 UA 中的 Windows NT 版本号以兼容识别 Windows 11
69 | * https://learn.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11
70 | * 替换 UA 中的 macOS 版本以兼容识别 Catalina 以上版本的 macOS
71 | */
72 | const getUserAgent = async () => {
73 | let ua = window.navigator.userAgent
74 | try {
75 | const { platform } = navigator.userAgentData
76 | if (platform === 'Windows' || platform === 'macOS') {
77 | const { platformVersion } = await navigator.userAgentData.getHighEntropyValues(['platformVersion'])
78 | const majorPlatformVersion = parseInt(platformVersion.split('.')[0])
79 | if (platform === 'Windows' && majorPlatformVersion >= 13) {
80 | const correctVersion = '11.0'
81 | ua = ua.replace(/Windows NT 10\.0/i, `Windows NT ${correctVersion}`)
82 | } else if (platform === 'macOS' && majorPlatformVersion >= 11) {
83 | const correctVersion = platformVersion.replace(/\./g, '_')
84 | ua = ua.replace(/Mac OS X 10_[0-9]+_[0-9]+/i, `Mac OS X ${correctVersion}`)
85 | }
86 | }
87 | } catch (e) {}
88 | return ua
89 | }
90 |
91 | /**
92 | * 由于 Twikoo 早期版本将 path 视为表达式处理,
93 | * 而其他同类评论系统都是把 path 视为字符串常量,
94 | * 为同时兼顾早期版本和统一性,就有了这个方法。
95 | */
96 | const getUrl = (path) => {
97 | let url
98 | if (window.TWIKOO_MAGIC_PATH) {
99 | // 从全局变量获取 path
100 | url = window.TWIKOO_MAGIC_PATH
101 | } else if (path && typeof path === 'string') {
102 | switch (path) {
103 | case 'location.pathname':
104 | case 'window.location.pathname':
105 | url = window.location.pathname
106 | break
107 | case 'location.href':
108 | case 'window.location.href':
109 | url = window.location.href
110 | break
111 | default:
112 | url = path
113 | }
114 | } else {
115 | // 默认 path
116 | url = window.location.pathname
117 | }
118 | return url
119 | }
120 |
121 | const getHref = (href) => {
122 | return window.TWIKOO_MAGIC_HREF ?? href ?? window.location.href
123 | }
124 |
125 | /**
126 | * 读取文本文件内容
127 | */
128 | const readAsText = (file) => {
129 | return new Promise((resolve, reject) => {
130 | const reader = new FileReader()
131 | reader.readAsText(file)
132 | reader.onloadend = () => {
133 | if (reader.error) {
134 | reject(reader.error)
135 | } else {
136 | resolve(reader.result)
137 | }
138 | }
139 | })
140 | }
141 |
142 | const renderLinks = (el) => {
143 | let aEls = []
144 | if (el instanceof Array) {
145 | el.forEach((item) => {
146 | aEls.push(...item.getElementsByTagName('a'))
147 | })
148 | } else if (el instanceof Element) {
149 | aEls = el.getElementsByTagName('a')
150 | }
151 | for (const aEl of aEls) {
152 | aEl.setAttribute('target', '_blank')
153 | aEl.setAttribute('rel', 'noopener noreferrer')
154 | }
155 | }
156 |
157 | const renderMath = (el, options) => {
158 | const defaultOptions = {
159 | delimiters: [
160 | { left: '$$', right: '$$', display: true },
161 | { left: '$', right: '$', display: false },
162 | { left: '\\(', right: '\\)', display: false },
163 | { left: '\\[', right: '\\]', display: true }
164 | ],
165 | throwOnError: false
166 | }
167 | if (typeof renderMathInElement === 'function') {
168 | /* eslint-disable-next-line no-undef */
169 | renderMathInElement(el, options || defaultOptions)
170 | }
171 | }
172 |
173 | const blobToDataURL = (blob) => {
174 | return new Promise((resolve) => {
175 | const reader = new FileReader()
176 | reader.onload = (evt) => {
177 | const base64 = evt.target.result
178 | resolve(base64)
179 | }
180 | reader.readAsDataURL(blob)
181 | })
182 | }
183 |
184 | export {
185 | t,
186 | setLanguage,
187 | isNotSet,
188 | logger,
189 | timeago,
190 | timestamp,
191 | convertLink,
192 | marked,
193 | renderCode,
194 | isUrl,
195 | call,
196 | getFuncVer,
197 | normalizeMail,
198 | isQQ,
199 | getQQAvatar,
200 | initOwoEmotions,
201 | initMarkedOwo,
202 | getCommentsCountApi,
203 | getRecentCommentsApi,
204 | getUserAgent,
205 | getUrl,
206 | getHref,
207 | readAsText,
208 | renderLinks,
209 | renderMath,
210 | blobToDataURL
211 | }
212 |
--------------------------------------------------------------------------------
/src/client/utils/marked.js:
--------------------------------------------------------------------------------
1 | import { marked } from '../lib/marked/marked'
2 |
3 | /**
4 | * https://marked.js.org/#/USING_ADVANCED.md
5 | */
6 | marked.setOptions({
7 | renderer: new marked.Renderer(),
8 | gfm: true,
9 | tables: true,
10 | breaks: true,
11 | pedantic: false,
12 | smartLists: true,
13 | smartypants: true
14 | })
15 |
16 | export default marked
17 |
--------------------------------------------------------------------------------
/src/client/utils/tcb.js:
--------------------------------------------------------------------------------
1 | import {
2 | isNotSet,
3 | logger
4 | } from '.'
5 |
6 | const builtInOptions = [
7 | { key: 'envId', required: true }
8 | ]
9 |
10 | const tcb = {
11 | sdk: null,
12 | app: null,
13 | auth: null
14 | }
15 |
16 | async function install (tcbSdk, options = {}) {
17 | tcb.sdk = tcbSdk
18 | checkOptions(options)
19 | await init(options)
20 | return tcb
21 | }
22 |
23 | function checkOptions (options) {
24 | const missingOptions = []
25 | for (const option of builtInOptions) {
26 | if (option.default && isNotSet(options[option.key])) {
27 | options[option.key] = option.default
28 | } else if (option.required && isNotSet(options[option.key])) {
29 | missingOptions.push(option.key)
30 | }
31 | }
32 | if (missingOptions.length > 0) {
33 | for (const missingOption of missingOptions) {
34 | logger.warn(`${missingOption} is required`)
35 | }
36 | throw new Error('Twikoo: failed to init')
37 | }
38 | }
39 |
40 | async function init (options) {
41 | initApp(options)
42 | await initAuth()
43 | }
44 |
45 | function initApp (options) {
46 | tcb.app = tcb.sdk.init({
47 | env: options.envId,
48 | region: options.region
49 | })
50 | }
51 |
52 | async function initAuth () {
53 | return new Promise((resolve, reject) => {
54 | tcb.auth = tcb.app.auth({ persistence: 'local' })
55 | if (tcb.auth.hasLoginState()) {
56 | resolve()
57 | } else {
58 | tcb.auth
59 | .anonymousAuthProvider()
60 | .signIn()
61 | .then(resolve)
62 | .catch(reject)
63 | }
64 | })
65 | }
66 |
67 | export {
68 | tcb,
69 | install
70 | }
71 |
--------------------------------------------------------------------------------
/src/client/utils/timeago.js:
--------------------------------------------------------------------------------
1 | import { logger, t } from '.'
2 |
3 | const timeAgo = (date) => {
4 | if (typeof date === 'number') {
5 | date = new Date(date)
6 | }
7 | if (date) {
8 | try {
9 | const oldTime = date.getTime()
10 | const currTime = Date.now()
11 | const diffValue = currTime - oldTime
12 |
13 | const days = Math.floor(diffValue / (24 * 3600 * 1000))
14 | if (days === 0) {
15 | // 计算相差小时数
16 | const leave1 = diffValue % (24 * 3600 * 1000) // 计算天数后剩余的毫秒数
17 | const hours = Math.floor(leave1 / (3600 * 1000))
18 | if (hours === 0) {
19 | // 计算相差分钟数
20 | const leave2 = leave1 % (3600 * 1000) // 计算小时数后剩余的毫秒数
21 | const minutes = Math.floor(leave2 / (60 * 1000))
22 | if (minutes === 0) {
23 | // 计算相差秒数
24 | const leave3 = leave2 % (60 * 1000) // 计算分钟数后剩余的毫秒数
25 | const seconds = Math.round(leave3 / 1000)
26 | return seconds + ` ${t('TIMEAGO_SECONDS')}`
27 | }
28 | return minutes + ` ${t('TIMEAGO_MINUTES')}`
29 | }
30 | return hours + ` ${t('TIMEAGO_HOURS')}`
31 | }
32 | if (days < 0) return t('TIMEAGO_NOW')
33 |
34 | if (days < 8) {
35 | return days + ` ${t('TIMEAGO_DAYS')}`
36 | } else {
37 | return dateFormat(date)
38 | }
39 | } catch (error) {
40 | logger.log('timeAgo 错误', error)
41 | }
42 | }
43 | }
44 |
45 | const dateFormat = (date) => {
46 | const vDay = padWithZeros(date.getDate(), 2)
47 | const vMonth = padWithZeros(date.getMonth() + 1, 2)
48 | const vYear = padWithZeros(date.getFullYear(), 2)
49 | return `${vYear}-${vMonth}-${vDay}`
50 | }
51 |
52 | const padWithZeros = (vNumber, width) => {
53 | let numAsString = vNumber.toString()
54 | while (numAsString.length < width) {
55 | numAsString = '0' + numAsString
56 | }
57 | return numAsString
58 | }
59 |
60 | export default timeAgo
61 |
--------------------------------------------------------------------------------
/src/client/version.js:
--------------------------------------------------------------------------------
1 | const version = '1.6.44'
2 |
3 | export { version }
4 |
--------------------------------------------------------------------------------
/src/client/view/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
33 |
34 |
127 |
--------------------------------------------------------------------------------
/src/client/view/components/TkAction.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
56 |
57 |
94 |
--------------------------------------------------------------------------------
/src/client/view/components/TkAdminExport.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ t('ADMIN_EXPORT_WARN') }}
5 |
6 |
{{ t('ADMIN_EXPORT_COMMENT') }}
7 |
{{ t('ADMIN_EXPORT_COUNTER') }}
8 |
9 |
10 |
11 |
47 |
--------------------------------------------------------------------------------
/src/client/view/components/TkAdminImport.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ t('ADMIN_IMPORT_WARN') }}
5 |
{{ warnText[source] }}
6 |
7 |
{{ t('ADMIN_IMPORT_SELECT_SOURCE') }}
8 |
16 |
{{ t('ADMIN_IMPORT_SELECT_FILE') }}
17 |
18 |
{{ t('ADMIN_IMPORT_START') }}
19 |
20 |
21 |
22 |
23 |
107 |
108 |
125 |
--------------------------------------------------------------------------------
/src/client/view/components/TkAvatar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
![]()
5 |
6 |
7 |
8 |
68 |
69 |
99 |
--------------------------------------------------------------------------------
/src/client/view/components/TkComments.vue:
--------------------------------------------------------------------------------
1 |
2 |
32 |
33 |
34 |
123 |
124 |
192 |
--------------------------------------------------------------------------------
/src/client/view/components/TkFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
42 |
43 |
52 |
--------------------------------------------------------------------------------
/src/client/view/components/TkMetaInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 | {{ metaInput.locale }}
12 |
13 |
14 |
15 |
16 |
153 |
154 |
186 |
--------------------------------------------------------------------------------
/src/client/view/components/TkPagination.vue:
--------------------------------------------------------------------------------
1 |
2 |
37 |
38 |
39 |
114 |
115 |
159 |
--------------------------------------------------------------------------------
/src/client/view/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App.vue'
3 | import Button from 'element-ui/lib/button'
4 | import Input from 'element-ui/lib/input'
5 | import Loading from 'element-ui/lib/loading'
6 | import 'element-ui/lib/theme-chalk/button.css'
7 | import 'element-ui/lib/theme-chalk/input.css'
8 | import 'element-ui/lib/theme-chalk/loading.css'
9 | import '../lib/owo.css'
10 |
11 | Vue.use(Button)
12 | Vue.use(Input)
13 | Vue.use(Loading)
14 |
15 | let app = null
16 |
17 | const render = (tcb, options = {}) => {
18 | Vue.prototype.$tcb = tcb
19 | Vue.prototype.$twikoo = options
20 | app = new Vue({ render: h => h(App) })
21 | app.$mount(options.el || '#twikoo')
22 | return app
23 | }
24 |
25 | export {
26 | app,
27 | render
28 | }
29 |
--------------------------------------------------------------------------------
/src/server/aws-lambda/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-present iMaeGoo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/server/aws-lambda/README.md:
--------------------------------------------------------------------------------
1 | # AWS Lambda
2 |
3 | Deploy Twikoo to [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html).
4 |
5 | ## Deploy with Terraform
6 |
7 | ```bash
8 | cd terraform
9 |
10 | # Init Terraform modules
11 | terraform init
12 |
13 | # Deploy to AWS
14 | terraform apply
15 | ```
16 |
--------------------------------------------------------------------------------
/src/server/aws-lambda/src/index.js:
--------------------------------------------------------------------------------
1 | const twikoo = require('twikoo-vercel')
2 |
3 | /*
4 | AWS Lambda compat layer for Vercel
5 | https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html
6 | */
7 |
8 | exports.handler = async function (event, context) {
9 | process.env.VERCEL_URL = event.requestContext.domainName
10 | process.env.TWIKOO_IP_HEADERS = JSON.stringify([
11 | 'headers.requestContext.http.sourceIp'
12 | ])
13 | const result = {
14 | statusCode: 204,
15 | headers: {},
16 | body: ''
17 | }
18 | const request = {
19 | method: event.requestContext.http.method,
20 | headers: event.headers,
21 | body: {}
22 | }
23 | try {
24 | if (event.isBase64Encoded) {
25 | request.body = JSON.parse(Buffer.from(event.body, 'base64').toString('utf-8'))
26 | } else {
27 | request.body = JSON.parse(event.body)
28 | }
29 | } catch (e) {}
30 | const response = {
31 | status: function (code) {
32 | result.statusCode = code
33 | return this
34 | },
35 | json: function (json) {
36 | result.headers['Content-Type'] = 'application/json'
37 | result.body = JSON.stringify(json)
38 | return this
39 | },
40 | end: function () {
41 | return this
42 | },
43 | setHeader: function (k, v) {
44 | result.headers[k] = v
45 | return this
46 | }
47 | }
48 | await twikoo(request, response)
49 | return result
50 | }
51 |
--------------------------------------------------------------------------------
/src/server/aws-lambda/src/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twikoo-aws-lambda",
3 | "version": "1.6.44",
4 | "description": "A simple comment system.",
5 | "author": "imaegoo (https://github.com/imaegoo)",
6 | "license": "MIT",
7 | "main": "index.js",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/twikoojs/twikoo.git"
11 | },
12 | "homepage": "https://twikoo.js.org",
13 | "dependencies": {
14 | "twikoo-vercel": "latest"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/server/aws-lambda/terraform/.gitignore:
--------------------------------------------------------------------------------
1 | .terraform*
2 | *.tfstate
3 | *.tfstate.backup
4 | builds/
5 |
--------------------------------------------------------------------------------
/src/server/aws-lambda/terraform/main.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | aws = {
4 | source = "hashicorp/aws"
5 | version = ">= 4.0"
6 | }
7 | }
8 | }
9 |
10 | provider "aws" {
11 | region = var.region
12 | }
13 |
14 | module "lambda_function" {
15 | source = "terraform-aws-modules/lambda/aws"
16 | version = "7.4.0"
17 |
18 | function_name = "twikoo"
19 | handler = "index.handler"
20 | runtime = "nodejs20.x"
21 | timeout = 60
22 |
23 | source_path = "../src"
24 |
25 | environment_variables = {
26 | MONGODB_URI = var.mongodb_uri
27 | }
28 |
29 | create_lambda_function_url = true
30 | }
31 |
32 | output "lambda_function_url" {
33 | value = module.lambda_function.lambda_function_url
34 | }
35 |
--------------------------------------------------------------------------------
/src/server/aws-lambda/terraform/variables.tf:
--------------------------------------------------------------------------------
1 | variable "region" {
2 | description = "AWS region to deploy the function in."
3 | default = "us-west-2"
4 | }
5 |
6 | variable "mongodb_uri" {
7 | description = "MongoDB connection URI. The value will be passed to the Lambda function as environment variable MONGODB_URI."
8 | sensitive = true
9 | }
10 |
--------------------------------------------------------------------------------
/src/server/deta/README.md:
--------------------------------------------------------------------------------
1 | # Deta
2 |
3 | 本目录存储 Deta Space serverless functions 代码
4 |
5 | ## 开发帮助文档
6 |
7 | https://deta.space/docs/en/build/new-apps
8 |
9 | https://deta.space/docs/en/build/reference/cli
10 |
11 | https://deta.space/docs/en/build/reference/spacefile
12 |
13 | https://deta.space/docs/en/build/quick-starts/node
14 |
15 | https://docs.mongodb.com/drivers/node/quick-start/
16 |
17 | http://mongodb.github.io/node-mongodb-native/3.6/api/
--------------------------------------------------------------------------------
/src/server/deta/Spacefile:
--------------------------------------------------------------------------------
1 | # Spacefile Docs: https://go.deta.dev/docs/spacefile/v0
2 | v: 0
3 | micros:
4 | - name: twikoo-deta
5 | src: ./
6 | engine: nodejs16
7 | public: true
8 | presets:
9 | env:
10 | - name: MONGODB_URI
11 | description: Twikoo 评论系统 MongoDB 数据库 URI
12 | default: ""
--------------------------------------------------------------------------------
/src/server/deta/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Deta 兼容 Vercel 函数实现
3 | * 复用 Twikoo Vercel 函数代码
4 | */
5 |
6 | const twikoo = require('twikoo-vercel')
7 | const express = require('express')
8 | const app = express()
9 |
10 | // Tip: Deta 本身无法获取评论者 IP,需要使用 Cloudflare CDN 才能获取评论者 IP。
11 | // Docs: https://deta.space/docs/en/build/guides/accessing-client-ip-address
12 | process.env.TWIKOO_IP_HEADERS = JSON.stringify(['headers.cf-connecting-ip'])
13 |
14 | app.use(async function (req, res) {
15 | const buffers = []
16 | req.on('data', (chunk) => {
17 | buffers.push(chunk)
18 | })
19 | req.on('end', async () => {
20 | try {
21 | req.body = JSON.parse(Buffer.concat(buffers).toString())
22 | } catch (e) {
23 | req.body = {}
24 | }
25 | res.status = function (code) {
26 | res.statusCode = code
27 | return this
28 | }
29 | res.json = function (json) {
30 | if (!res.headersSent) {
31 | res.setHeader('Content-Type', 'application/json')
32 | res.status(200).send(JSON.stringify(json))
33 | }
34 | return this
35 | }
36 | return await twikoo(req, res)
37 | })
38 | })
39 |
40 | app.listen(8080)
41 |
--------------------------------------------------------------------------------
/src/server/deta/package.json:
--------------------------------------------------------------------------------
1 | { "dependencies": { "twikoo-vercel": "latest","express": "latest" } }
--------------------------------------------------------------------------------
/src/server/function/README.md:
--------------------------------------------------------------------------------
1 | # 腾讯云函数
2 |
3 | 本目录存储腾讯云函数代码
4 |
5 | ## 帮助文档
6 |
7 | https://cloud.tencent.com/document/product/876/46798
8 |
9 | ## SDK
10 |
11 | https://docs.cloudbase.net/api-reference/server/node-sdk/introduction.html
12 |
13 | ## CLI 文档
14 |
15 | https://docs.cloudbase.net/cli-v1/intro.html
16 |
--------------------------------------------------------------------------------
/src/server/function/twikoo/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-present iMaeGoo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/server/function/twikoo/README.md:
--------------------------------------------------------------------------------
1 | # Twikoo 云函数
2 |
3 | [](https://www.npmjs.com/package/twikoo-func)
4 | [](./LICENSE)
5 |
6 | 使用说明:https://twikoo.js.org
7 |
--------------------------------------------------------------------------------
/src/server/function/twikoo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twikoo-func",
3 | "version": "1.6.44",
4 | "description": "A simple comment system.",
5 | "author": "imaegoo (https://github.com/imaegoo)",
6 | "license": "MIT",
7 | "main": "index.js",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/twikoojs/twikoo.git"
11 | },
12 | "homepage": "https://twikoo.js.org",
13 | "dependencies": {
14 | "@cloudbase/manager-node": "^3.9.0",
15 | "@cloudbase/node-sdk": "^2.5.0",
16 | "@imaegoo/node-ip2region": "^2.1.1",
17 | "akismet-api": "^5.1.0",
18 | "axios": "^1.6.2",
19 | "blueimp-md5": "^2.18.0",
20 | "bowser": "^2.11.0",
21 | "cheerio": "1.0.0-rc.5",
22 | "crypto-js": "^4.0.0",
23 | "dompurify": "^2.2.6",
24 | "form-data": "^4.0.0",
25 | "jsdom": "^16.4.0",
26 | "marked": "^4.0.12",
27 | "nodemailer": "^6.4.17",
28 | "pushoo": "latest",
29 | "tencentcloud-sdk-nodejs": "^4.0.65",
30 | "xml2js": "^0.6.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/server/function/twikoo/utils/constants.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | RES_CODE: {
3 | SUCCESS: 0,
4 | NO_PARAM: 100,
5 | FAIL: 1000,
6 | EVENT_NOT_EXIST: 1001,
7 | PASS_EXIST: 1010,
8 | CONFIG_NOT_EXIST: 1020,
9 | CREDENTIALS_NOT_EXIST: 1021,
10 | CREDENTIALS_INVALID: 1025,
11 | PASS_NOT_EXIST: 1022,
12 | PASS_NOT_MATCH: 1023,
13 | NEED_LOGIN: 1024,
14 | FORBIDDEN: 1403,
15 | AKISMET_ERROR: 1030,
16 | UPLOAD_FAILED: 1040
17 | },
18 | MAX_REQUEST_TIMES: parseInt(process.env.TWIKOO_THROTTLE) || 250
19 | }
20 |
--------------------------------------------------------------------------------
/src/server/function/twikoo/utils/image.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const os = require('os')
3 | const path = require('path')
4 | const { isUrl } = require('.')
5 | const { RES_CODE } = require('./constants')
6 | const { getAxios, getFormData } = require('./lib')
7 | const axios = getAxios()
8 | const FormData = getFormData()
9 | const logger = require('./logger')
10 |
11 | const fn = {
12 | async uploadImage (event, config) {
13 | const { photo, fileName } = event
14 | const res = {}
15 | try {
16 | if (!config.IMAGE_CDN || !config.IMAGE_CDN_TOKEN) {
17 | throw new Error('未配置图片上传服务')
18 | }
19 | // tip: qcloud 图床走前端上传,其他图床走后端上传
20 | if (config.IMAGE_CDN === '7bu') {
21 | await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: 'https://7bu.top' })
22 | } else if (config.IMAGE_CDN === 'smms') {
23 | await fn.uploadImageToSmms({ photo, fileName, config, res, imageCdn: 'https://smms.app/api/v2/upload' })
24 | } else if (isUrl(config.IMAGE_CDN)) {
25 | await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN })
26 | } else if (config.IMAGE_CDN === 'lskypro') {
27 | await fn.uploadImageToLskyPro({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN_URL })
28 | } else if (config.IMAGE_CDN === 'piclist') {
29 | await fn.uploadImageToPicList({ photo, fileName, config, res, imageCdn: config.IMAGE_CDN_URL })
30 | } else if (config.IMAGE_CDN === 'easyimage') {
31 | await fn.uploadImageToEasyImage({ photo, fileName, config, res })
32 | } else {
33 | throw new Error('不支持的图片上传服务')
34 | }
35 | } catch (e) {
36 | logger.error(e)
37 | res.code = RES_CODE.UPLOAD_FAILED
38 | res.err = e.message
39 | }
40 | return res
41 | },
42 | async uploadImageToSmms ({ photo, fileName, config, res, imageCdn }) {
43 | // SM.MS 图床 https://sm.ms
44 | const formData = new FormData()
45 | formData.append('smfile', fn.base64UrlToReadStream(photo, fileName))
46 | const uploadResult = await axios.post(imageCdn, formData, {
47 | headers: {
48 | ...formData.getHeaders(),
49 | Authorization: config.IMAGE_CDN_TOKEN
50 | }
51 | })
52 | if (uploadResult.data.success) {
53 | res.data = uploadResult.data.data
54 | } else {
55 | throw new Error(uploadResult.data.message)
56 | }
57 | },
58 | async uploadImageToLskyPro ({ photo, fileName, config, res, imageCdn }) {
59 | // 自定义兰空图床(v2)URL
60 | const formData = new FormData()
61 | formData.append('file', fn.base64UrlToReadStream(photo, fileName))
62 | if (process.env.TWIKOO_LSKY_STRATEGY_ID) {
63 | formData.append('strategy_id', parseInt(process.env.TWIKOO_LSKY_STRATEGY_ID))
64 | }
65 | const url = `${imageCdn}/api/v1/upload`
66 | let token = config.IMAGE_CDN_TOKEN
67 | if (!token.startsWith('Bearer')) {
68 | token = `Bearer ${token}`
69 | }
70 | const uploadResult = await axios.post(url, formData, {
71 | headers: {
72 | ...formData.getHeaders(),
73 | Authorization: token
74 | }
75 | })
76 | if (uploadResult.data.status) {
77 | res.data = uploadResult.data.data
78 | res.data.url = res.data.links.url
79 | } else {
80 | throw new Error(uploadResult.data.message)
81 | }
82 | },
83 | async uploadImageToPicList ({ photo, fileName, config, res, imageCdn }) {
84 | // PicList https://piclist.cn/ 高效的云存储和图床平台管理工具
85 | // 鉴权使用 query 参数 key
86 | const formData = new FormData()
87 | formData.append('file', fn.base64UrlToReadStream(photo, fileName))
88 | let url = `${imageCdn}/upload`
89 | // 如果填写了 key 则拼接 url
90 | if (config.IMAGE_CDN_TOKEN) {
91 | url += `?key=${config.IMAGE_CDN_TOKEN}`
92 | }
93 | const uploadResult = await axios.post(url, formData)
94 | if (uploadResult.data.success) {
95 | res.data = uploadResult.data
96 | res.data.url = uploadResult.data.result[0]
97 | } else {
98 | throw new Error(uploadResult.data.message)
99 | }
100 | },
101 | async uploadImageToEasyImage ({ photo, fileName, config, res }) {
102 | // EasyImage2.0 https://github.com/icret/EasyImages2.0 简单图床 - 一款功能强大无数据库的图床 2.0版
103 | try {
104 | // 参数校验
105 | if (!config.IMAGE_CDN_URL) {
106 | throw new Error('未配置 EasyImage2.0 的 API 地址 (IMAGE_CDN_URL)')
107 | }
108 | if (!config.IMAGE_CDN_TOKEN) {
109 | throw new Error('未配置 EasyImage2.0 的 Token (IMAGE_CDN_TOKEN)')
110 | }
111 | // 构建固定格式的 FormData
112 | const formData = new FormData()
113 | // 添加 token 参数到 Body
114 | formData.append('token', config.IMAGE_CDN_TOKEN)
115 | // 添加图片文件(固定参数名 image)
116 | formData.append('image', fn.base64UrlToReadStream(photo, fileName), {
117 | filename: fileName
118 | })
119 | // 发送请求
120 | const uploadResult = await axios.post(config.IMAGE_CDN_URL, formData, {
121 | headers: {
122 | ...formData.getHeaders(),
123 | 'User-Agent': 'Twikoo'
124 | }
125 | })
126 | // 解析响应
127 | const response = uploadResult.data
128 | // 检查业务状态码
129 | if (response.code !== 200 || response.result !== 'success') {
130 | throw new Error(`API 返回错误 (CODE: ${response.code})`)
131 | }
132 | // 提取图片 URL(固定 JSON 路径 url)
133 | if (!response.url) {
134 | throw new Error('未找到有效图片 URL')
135 | }
136 | // 返回标准化结构
137 | res.data = {
138 | url: response.url,
139 | thumb: response.thumb, // 可选返回缩略图
140 | del: response.del // 可选返回删除链接
141 | }
142 | } catch (e) {
143 | let errorMsg = `EasyImage2.0 上传失败: ${e.message}`
144 | // 追加 API 返回的错误详情
145 | if (e.response?.data) {
146 | errorMsg += ` | 错误类型: ${e.response.data.message || '未知'}`
147 | }
148 | throw new Error(errorMsg)
149 | }
150 | },
151 | base64UrlToReadStream (base64Url, fileName) {
152 | const base64 = base64Url.split(';base64,').pop()
153 | const writePath = path.resolve(os.tmpdir(), fileName)
154 | fs.writeFileSync(writePath, base64, { encoding: 'base64' })
155 | return fs.createReadStream(writePath)
156 | }
157 | }
158 |
159 | module.exports = fn
160 |
--------------------------------------------------------------------------------
/src/server/function/twikoo/utils/lib.js:
--------------------------------------------------------------------------------
1 | let customLibs = {}
2 |
3 | module.exports = {
4 | setCustomLibs (libs) {
5 | customLibs = libs
6 | },
7 | getCheerio () {
8 | const $ = require('cheerio') // jQuery 服务器版
9 | return $
10 | },
11 | getAkismetClient () {
12 | const { AkismetClient } = require('akismet-api') // 反垃圾 API
13 | return AkismetClient
14 | },
15 | getCryptoJS () {
16 | const CryptoJS = require('crypto-js') // 编解码
17 | return CryptoJS
18 | },
19 | getFormData () {
20 | const FormData = require('form-data') // 图片上传
21 | return FormData
22 | },
23 | getAxios () {
24 | const axios = require('axios') // 发送 REST 请求
25 | return axios
26 | },
27 | getBowser () {
28 | const bowser = require('bowser') // UserAgent 格式化
29 | return bowser
30 | },
31 | getDomPurify () {
32 | if (customLibs.DOMPurify) return customLibs.DOMPurify
33 | // 初始化反 XSS
34 | const { JSDOM } = require('jsdom') // document.window 服务器版
35 | const createDOMPurify = require('dompurify') // 反 XSS
36 | const window = new JSDOM('').window
37 | const DOMPurify = createDOMPurify(window)
38 | return DOMPurify
39 | },
40 | getIpToRegion () {
41 | const ipToRegion = require('@imaegoo/node-ip2region') // IP 属地查询
42 | return ipToRegion
43 | },
44 | getMarked () {
45 | const marked = require('marked') // Markdown 解析
46 | return marked
47 | },
48 | getMd5 () {
49 | const md5 = require('blueimp-md5') // MD5 哈希
50 | return md5
51 | },
52 | getSha256 () {
53 | const { SHA256 } = require('crypto-js') // SHA256 哈希
54 | return (message) => {
55 | return SHA256(message).toString()
56 | }
57 | },
58 | getNodemailer () {
59 | if (customLibs.nodemailer) return customLibs.nodemailer
60 | const nodemailer = require('nodemailer') // 发送邮件
61 | return nodemailer
62 | },
63 | getPushoo () {
64 | const pushoo = require('pushoo').default // 即时消息通知
65 | return pushoo
66 | },
67 | getTencentcloud () {
68 | const tencentcloud = require('tencentcloud-sdk-nodejs') // 腾讯云 API NODEJS SDK
69 | return tencentcloud
70 | },
71 | getXml2js () {
72 | const xml2js = require('xml2js') // XML 解析
73 | return xml2js
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/server/function/twikoo/utils/logger.js:
--------------------------------------------------------------------------------
1 | let envLogLevel = process.env.TWIKOO_LOG_LEVEL || 'info'
2 | envLogLevel = envLogLevel.toLowerCase()
3 | const logLevel = { verbose: 1, info: 2, warn: 3, error: 4 }[envLogLevel] || 2
4 |
5 | const logger = {
6 | log: (...messages) => {
7 | if (logLevel <= 1) console.log(logPrefix(), ...messages)
8 | },
9 | info: (...messages) => {
10 | if (logLevel <= 2) console.info(logPrefix(), ...messages)
11 | },
12 | warn: (...messages) => {
13 | if (logLevel <= 3) console.warn(logPrefix(), ...messages)
14 | },
15 | error: (...messages) => {
16 | if (logLevel <= 4) console.error(logPrefix(), ...messages)
17 | }
18 | }
19 |
20 | const logPrefix = () => `${new Date().toLocaleString()} Twikoo:`
21 |
22 | module.exports = logger
23 |
--------------------------------------------------------------------------------
/src/server/function/twikoo/utils/spam.js:
--------------------------------------------------------------------------------
1 | const {
2 | getAkismetClient,
3 | getCryptoJS,
4 | getTencentcloud
5 | } = require('./lib')
6 | const {
7 | equalsMail
8 | } = require('.')
9 | const AkismetClient = getAkismetClient()
10 | const CryptoJS = getCryptoJS()
11 |
12 | const logger = require('./logger')
13 |
14 | let tencentcloud
15 |
16 | function getTencentCloud () {
17 | if (!tencentcloud) {
18 | try {
19 | tencentcloud = getTencentcloud() // 腾讯云 API NODEJS SDK
20 | } catch (e) {
21 | logger.warn('加载 "tencentcloud-sdk-nodejs" 失败', e)
22 | }
23 | }
24 | return tencentcloud
25 | }
26 |
27 | const fn = {
28 | // 后垃圾评论检测
29 | async postCheckSpam (comment, config) {
30 | try {
31 | let isSpam
32 | if (comment.isSpam) {
33 | // 预检测没过的,就不再检测了
34 | isSpam = true
35 | } else if (equalsMail(config.BLOGGER_EMAIL, comment.mail)) {
36 | // 博主本人评论,不再检测了
37 | isSpam = false
38 | } else if (config.QCLOUD_SECRET_ID && config.QCLOUD_SECRET_KEY) {
39 | // 腾讯云内容安全
40 | const client = new (getTencentCloud().tms.v20201229.Client)({
41 | credential: { secretId: config.QCLOUD_SECRET_ID, secretKey: config.QCLOUD_SECRET_KEY },
42 | region: 'ap-shanghai',
43 | profile: { httpProfile: { endpoint: 'tms.tencentcloudapi.com' } }
44 | })
45 | const textModerationParams = {
46 | // 文档: https://cloud.tencent.com/document/api/1124/51860
47 | Content: CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(comment.comment)),
48 | DataId: comment.id,
49 | User: { Nickname: comment.nick },
50 | Device: { IP: comment.ip }
51 | }
52 | if (config.QCLOUD_CMS_BIZTYPE) {
53 | textModerationParams.BizType = config.QCLOUD_CMS_BIZTYPE
54 | }
55 | logger.log('腾讯云请求参数:', textModerationParams)
56 | const checkResult = await client.TextModeration(textModerationParams)
57 | logger.log('腾讯云返回结果:', checkResult)
58 | isSpam = checkResult.Suggestion !== 'Pass'
59 | } else if (config.AKISMET_KEY) {
60 | // Akismet
61 | const akismetClient = new AkismetClient({
62 | key: config.AKISMET_KEY,
63 | blog: config.SITE_URL
64 | })
65 | const isValid = await akismetClient.verifyKey()
66 | if (!isValid) {
67 | logger.warn('Akismet key 不可用:', config.AKISMET_KEY)
68 | return
69 | }
70 | isSpam = await akismetClient.checkSpam({
71 | user_ip: comment.ip,
72 | user_agent: comment.ua,
73 | permalink: comment.href,
74 | comment_type: comment.rid ? 'reply' : 'comment',
75 | comment_author: comment.nick,
76 | comment_author_email: comment.mail,
77 | comment_author_url: comment.link,
78 | comment_content: comment.comment
79 | })
80 | }
81 | logger.log('垃圾评论检测结果:', isSpam)
82 | return isSpam
83 | } catch (err) {
84 | logger.error('垃圾评论检测异常:', err)
85 | }
86 | }
87 | }
88 |
89 | module.exports = fn
90 |
--------------------------------------------------------------------------------
/src/server/hf-space/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-slim AS build
2 |
3 | RUN useradd -m app
4 |
5 | USER root
6 |
7 | RUN npm i -g tkserver
8 |
9 | ADD https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 /usr/local/bin/cloudflared
10 | RUN chmod +x /usr/local/bin/cloudflared
11 |
12 | COPY ./src/start.sh /usr/bin/start.sh
13 |
14 | USER app
15 | WORKDIR /home/app/twikoo
16 |
17 | CMD ["sh", "/usr/bin/start.sh"]
18 |
--------------------------------------------------------------------------------
/src/server/hf-space/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Twikoo Huggingface Space
3 | emoji: 📚
4 | colorFrom: yellow
5 | colorTo: indigo
6 | sdk: docker
7 | pinned: false
8 | app_port: 8080
9 | ---
--------------------------------------------------------------------------------
/src/server/hf-space/src/start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | nohup /usr/local/bin/cloudflared tunnel --no-autoupdate run --token $CF_ZERO_TRUST_TOKEN >> /dev/stdout 2>&1 &
4 | tkserver
5 |
--------------------------------------------------------------------------------
/src/server/netlify/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-present iMaeGoo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/server/netlify/README.md:
--------------------------------------------------------------------------------
1 | # Twikoo 云函数
2 |
3 | ## 部署
4 |
5 | 请查看 [Netlify 部署](https://twikoo.js.org/backend.html#netlify-部署)
6 |
--------------------------------------------------------------------------------
/src/server/netlify/netlify/functions/twikoo.js:
--------------------------------------------------------------------------------
1 | const twikoo = require('twikoo-vercel')
2 |
3 | /**
4 | * Netlify 函数兼容 Vercel 函数实现
5 | * 复用 Twikoo Vercel 函数代码
6 | * Netlify functions doc:
7 | * https://docs.netlify.com/functions/create/?fn-language=js
8 | */
9 | exports.handler = async function (event, context) {
10 | process.env.VERCEL_URL = event.rawUrl.replace(/^https?:\/\//, '')
11 | process.env.TWIKOO_IP_HEADERS = JSON.stringify([
12 | 'headers.x-nf-client-connection-ip'
13 | ])
14 | const result = {
15 | statusCode: 204,
16 | headers: {},
17 | body: ''
18 | }
19 | const request = {
20 | method: event.httpMethod,
21 | headers: event.headers,
22 | body: {}
23 | }
24 | try {
25 | request.body = JSON.parse(event.body)
26 | } catch (e) {}
27 | const response = {
28 | status: function (code) {
29 | result.statusCode = code
30 | return this
31 | },
32 | json: function (json) {
33 | result.headers['Content-Type'] = 'application/json'
34 | result.body = JSON.stringify(json)
35 | return this
36 | },
37 | end: function () {
38 | return this
39 | },
40 | setHeader: function (k, v) {
41 | result.headers[k] = v
42 | return this
43 | }
44 | }
45 | await twikoo(request, response)
46 | return result
47 | }
48 |
--------------------------------------------------------------------------------
/src/server/netlify/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twikoo-netlify",
3 | "version": "1.6.44",
4 | "description": "A simple comment system.",
5 | "author": "imaegoo (https://github.com/imaegoo)",
6 | "license": "MIT",
7 | "main": "netlify/functions/twikoo.js",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/twikoojs/twikoo.git"
11 | },
12 | "homepage": "https://twikoo.js.org",
13 | "dependencies": {
14 | "twikoo-vercel": "latest"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/server/pkg/.env:
--------------------------------------------------------------------------------
1 | # twikoo 配置
2 |
3 | # MongoDB 数据库连接字符串,不传则使用 lokijs
4 | MONGODB_URI=
5 |
6 | # MongoDB 数据库连接字符串,不传则使用 lokijs
7 | MONGO_URL=
8 |
9 | # lokijs 数据库存储路径
10 | TWIKOO_DATA= './data'
11 |
12 | # 端口号
13 | TWIKOO_PORT=8080
14 |
15 | # IP 请求限流,当同一 IP 短时间内请求次数超过阈值将对该 IP 返回错误
16 | TWIKOO_THROTTLE=250
17 |
18 | # 为true时只监听本地请求,使得 nginx 等服务器反代之后不暴露原始端口
19 | TWIKOO_LOCALHOST_ONLY=
20 |
21 | # 日志级别,支持 verbose / info / warn / error
22 | TWIKOO_LOG_LEVEL=info
23 |
24 | # 在一些特殊情况下使用,如使用了CloudFlare CDN 它会将请求 IP 写到请求头的 cf-connecting-ip 字段上,为了能够正确的获取请求 IP 你可以写成 ['headers.cf-connecting-ip']
25 | TWIKOO_IP_HEADERS=[]
--------------------------------------------------------------------------------
/src/server/pkg/.eslintignore:
--------------------------------------------------------------------------------
1 | *.css
2 | *.json
3 | *.md
4 | LICENSE
5 | src/client/lib/marked/*
6 | src/server/self-hosted/data/*
7 | yarn.lock
8 | patches/
9 | web.config
--------------------------------------------------------------------------------
/src/server/pkg/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | .DS_Store
12 | dist
13 | data
14 | !.env
15 |
16 | /cypress/videos/
17 | /cypress/screenshots/
18 |
19 | # Editor directories and files
20 | .idea
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
26 |
--------------------------------------------------------------------------------
/src/server/pkg/.npmrc:
--------------------------------------------------------------------------------
1 | # pnpm 依赖安装方式
2 | node-linker=hoisted
3 |
--------------------------------------------------------------------------------
/src/server/pkg/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "*.config": "xml",
4 | }
5 | }
--------------------------------------------------------------------------------
/src/server/pkg/README.md:
--------------------------------------------------------------------------------
1 | # twikoo 可执行文件
2 |
3 | ## 打包命令
4 |
5 | ```sh
6 | npm run build
7 | ```
8 |
9 | ## 可执行文件下载地址
10 |
11 | [latest](https://github.com/kongxiangyiren/twikoo-pkg/releases/latest)
12 |
13 | ## window iis 启动
14 |
15 | 1、[下载安装 aspNetCore](https://dotnet.microsoft.com/zh-cn/download/dotnet/thank-you/runtime-aspnetcore-8.0.0-windows-hosting-bundle-installer)
16 |
--------------------------------------------------------------------------------
/src/server/pkg/index.js:
--------------------------------------------------------------------------------
1 | const { program } = require('commander')
2 | const { join } = require('path')
3 | const { existsSync, writeFileSync, readFileSync } = require('fs')
4 |
5 | program
6 | .name(require('./package.json').name)
7 | .version(require('./package.json').dependencies.tkserver, '-v, --version')
8 | .description(
9 | `DESCRIPTION:
10 | Official website: https://twikoo.js.org/`
11 | )
12 | .addHelpCommand(false)
13 |
14 | program.parse(process.argv)
15 |
16 | // 创建.env文件
17 | if (
18 | process.pkg &&
19 | !existsSync(join(process.execPath, '..', '.env')) &&
20 | existsSync(join(__dirname, '.env'))
21 | ) {
22 | writeFileSync(
23 | join(process.execPath, '..', '.env'),
24 | readFileSync(join(__dirname, '.env'), 'utf-8')
25 | )
26 | }
27 |
28 | // 适配iis
29 | if (
30 | process.pkg &&
31 | process.platform === 'win32' &&
32 | !existsSync(join(process.execPath, '..', './web.config')) &&
33 | existsSync(join(__dirname, './web.config'))
34 | ) {
35 | writeFileSync(
36 | join(process.execPath, '..', './web.config'),
37 | readFileSync(join(__dirname, './web.config'), 'utf-8')
38 | )
39 | }
40 |
41 | // 获取env
42 | require('dotenv').config({
43 | path:
44 | process.pkg && existsSync(join(process.execPath, '..', '.env'))
45 | ? join(process.execPath, '..', '.env')
46 | : existsSync(join(__dirname, '.env'))
47 | ? join(__dirname, '.env')
48 | : undefined
49 | })
50 |
51 | // 匹配iis
52 | if (process.env.ASPNETCORE_PORT) {
53 | process.env.TWIKOO_PORT = process.env.ASPNETCORE_PORT
54 | process.env.TWIKOO_LOCALHOST_ONLY = null
55 | }
56 |
57 | require('tkserver')
58 |
--------------------------------------------------------------------------------
/src/server/pkg/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twikoo",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "bin": "index.js",
7 | "scripts": {
8 | "start": "node index.js",
9 | "postinstall": "patch-package",
10 | "diff":"patch-package tkserver",
11 | "build": "rimraf ./dist && pkg . -C GZip --no-bytecode --public-packages \"*\" --public"
12 | },
13 | "pkg": {
14 | "scripts": [],
15 | "assets": [
16 | "./node_modules/axios/dist/node/axios.cjs",
17 | "./node_modules/@imaegoo/node-ip2region/data/ip2region.db",
18 | "./web.config",
19 | "./.env"
20 | ],
21 | "targets": [
22 | "node18-win-x64",
23 | "node18-linux-x64",
24 | "node18-macos-x64"
25 | ],
26 | "outputPath": "dist"
27 | },
28 | "dependencies": {
29 | "commander": "^11.1.0",
30 | "dotenv": "^16.4.1",
31 | "tkserver": "1.6.44"
32 | },
33 | "devDependencies": {
34 | "patch-package": "^8.0.0",
35 | "pkg": "5.8.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/server/pkg/patches/pkg-fetch+3.4.2.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/pkg-fetch/lib-es5/index.js b/node_modules/pkg-fetch/lib-es5/index.js
2 | index 4b1339e..08d09ee 100644
3 | --- a/node_modules/pkg-fetch/lib-es5/index.js
4 | +++ b/node_modules/pkg-fetch/lib-es5/index.js
5 | @@ -79,7 +79,7 @@ function download(_a, local) {
6 | return __generator(this, function (_c) {
7 | switch (_c.label) {
8 | case 0:
9 | - url = "https://github.com/vercel/pkg-fetch/releases/download/" + tag + "/" + name;
10 | + url = "https://hub.gitmirror.com/https://github.com/vercel/pkg-fetch/releases/download/" + tag + "/" + name;
11 | _c.label = 1;
12 | case 1:
13 | _c.trys.push([1, 4, , 5]);
14 |
--------------------------------------------------------------------------------
/src/server/pkg/patches/tkserver+1.6.31.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/tkserver/index.js b/node_modules/tkserver/index.js
2 | index d1fb7c9..a0972a2 100644
3 | --- a/node_modules/tkserver/index.js
4 | +++ b/node_modules/tkserver/index.js
5 | @@ -210,7 +210,7 @@ function anonymousSignIn (request) {
6 |
7 | async function connectToDatabase () {
8 | if (db) return db
9 | - const dataDir = path.resolve(process.cwd(), process.env.TWIKOO_DATA || './data')
10 | + const dataDir = path.resolve(process.pkg ? path.join(process.execPath, '..') : process.cwd(), process.env.TWIKOO_DATA || './data')
11 | if (!fs.existsSync(dataDir)) {
12 | fs.mkdirSync(dataDir)
13 | }
14 |
--------------------------------------------------------------------------------
/src/server/pkg/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/server/self-hosted/README.md:
--------------------------------------------------------------------------------
1 | # Twikoo 私有部署服务端
2 |
3 | ## 安装
4 |
5 | ```
6 | npm i -g tkserver
7 | ```
8 |
9 | ## 启动
10 |
11 | ```
12 | tkserver
13 | ```
14 |
15 | ## 环境变量
16 |
17 | | 名称 | 描述 | 默认值 |
18 | | ---- | ---- | ---- |
19 | | `MONGODB_URI` | MongoDB 数据库连接字符串,不传则使用 lokijs | `null` |
20 | | `MONGO_URL` | MongoDB 数据库连接字符串,不传则使用 lokijs | `null` |
21 | | `TWIKOO_DATA` | lokijs 数据库存储路径 | `./data` |
22 | | `TWIKOO_PORT` | 端口号 | `8080` |
23 | | `TWIKOO_THROTTLE` | IP 请求限流,当同一 IP 短时间内请求次数超过阈值将对该 IP 返回错误 | `250` |
24 | | `TWIKOO_LOCALHOST_ONLY` | 为`true`时只监听本地请求,使得 nginx 等服务器反代之后不暴露原始端口 | `null` |
25 | | `TWIKOO_LOG_LEVEL` | 日志级别,支持 `verbose` / `info` / `warn` / `error` | `info` |
26 | | `TWIKOO_IP_HEADERS` | 在一些特殊情况下使用,如使用了`CloudFlare CDN` 它会将请求 IP 写到请求头的 `cf-connecting-ip` 字段上,为了能够正确的获取请求 IP 你可以写成 `['headers.cf-connecting-ip']` | `[]` |
27 |
--------------------------------------------------------------------------------
/src/server/self-hosted/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tkserver",
3 | "version": "1.6.44",
4 | "description": "A simple comment system.",
5 | "keywords": [
6 | "twikoo",
7 | "twikoojs",
8 | "comment",
9 | "comment-system"
10 | ],
11 | "author": "imaegoo (https://github.com/imaegoo)",
12 | "license": "MIT",
13 | "main": "server.js",
14 | "bin": {
15 | "tkserver": "server.js"
16 | },
17 | "scripts": {
18 | "build": "cd .",
19 | "start": "node server.js"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "https://github.com/twikoojs/twikoo.git"
24 | },
25 | "homepage": "https://twikoo.js.org",
26 | "publishConfig": {
27 | "access": "public",
28 | "registry": "https://registry.npmjs.org/"
29 | },
30 | "dependencies": {
31 | "get-user-ip": "^1.0.1",
32 | "lokijs": "^1.5.12",
33 | "mongodb": "^6.3.0",
34 | "twikoo-func": "1.6.44",
35 | "uuid": "^8.3.2"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/server/self-hosted/server.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const http = require('http')
4 | const logger = require('twikoo-func/utils/logger')
5 |
6 | const dbUrl = process.env.MONGODB_URI || process.env.MONGO_URL || null
7 | const twikoo = dbUrl ? require('./mongo') : require('./index')
8 | const server = http.createServer()
9 |
10 | server.on('request', async function (request, response) {
11 | try {
12 | const buffers = []
13 | for await (const chunk of request) {
14 | buffers.push(chunk)
15 | }
16 | request.body = JSON.parse(Buffer.concat(buffers).toString())
17 | } catch (e) {
18 | request.body = {}
19 | }
20 | response.status = function (code) {
21 | this.statusCode = code
22 | return this
23 | }
24 | response.json = function (json) {
25 | if (!response.writableEnded) {
26 | this.writeHead(200, { 'Content-Type': 'application/json' })
27 | this.end(JSON.stringify(json))
28 | }
29 | return this
30 | }
31 | return await twikoo(request, response)
32 | })
33 |
34 | const port = parseInt(process.env.TWIKOO_PORT) || 8080
35 | const host = process.env.TWIKOO_LOCALHOST_ONLY === 'true' ? 'localhost' : '::'
36 |
37 | server.listen(port, host, function () {
38 | logger.info(`Twikoo is using ${dbUrl ? 'mongo' : 'loki'} database`)
39 | logger.info(`Twikoo function started on host ${host} port ${port}`)
40 | })
41 |
--------------------------------------------------------------------------------
/src/server/vercel-min/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # Please see the documentation for all configuration options:
2 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
3 |
4 | version: 2
5 | updates:
6 | - package-ecosystem: "npm"
7 | directory: "/"
8 | schedule:
9 | interval: "daily"
10 |
--------------------------------------------------------------------------------
/src/server/vercel-min/api/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('twikoo-vercel')
2 |
--------------------------------------------------------------------------------
/src/server/vercel-min/package.json:
--------------------------------------------------------------------------------
1 | { "dependencies": { "twikoo-vercel": "latest" } }
2 |
--------------------------------------------------------------------------------
/src/server/vercel-min/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | { "source": "/(.*)", "destination": "api/index" }
4 | ]
5 | }
--------------------------------------------------------------------------------
/src/server/vercel/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-present iMaeGoo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/server/vercel/README.md:
--------------------------------------------------------------------------------
1 | # Vercel
2 |
3 | 本目录存储 Vercel serverless functions 代码
4 |
5 | ## 开发帮助文档
6 |
7 | https://vercel.com/docs/configuration
8 |
9 | https://vercel.com/docs/runtimes
10 |
11 | https://docs.mongodb.com/drivers/node/quick-start/
12 |
13 | http://mongodb.github.io/node-mongodb-native/3.6/api/
14 |
--------------------------------------------------------------------------------
/src/server/vercel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twikoo-vercel",
3 | "version": "1.6.44",
4 | "description": "A simple comment system.",
5 | "author": "imaegoo (https://github.com/imaegoo)",
6 | "license": "MIT",
7 | "main": "api/index.js",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/twikoojs/twikoo.git"
11 | },
12 | "homepage": "https://twikoo.js.org",
13 | "dependencies": {
14 | "get-user-ip": "^1.0.1",
15 | "mongodb": "^6.3.0",
16 | "twikoo-func": "1.6.44",
17 | "uuid": "^8.3.2"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/server/vercel/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | { "source": "/(.*)", "destination": "api/index" }
4 | ]
5 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const webpack = require('webpack')
3 | const CopyPlugin = require('copy-webpack-plugin')
4 | const VueLoaderPlugin = require('vue-loader/lib/plugin')
5 | const ROOT_PATH = path.resolve(__dirname)
6 | const BUILD_PATH = path.resolve(ROOT_PATH, 'dist')
7 | const version = require('./package.json').version
8 | const TerserPlugin = require('terser-webpack-plugin')
9 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
10 | const banner =
11 | 'Twikoo v' + version + '\n' +
12 | '(c) 2020-' + new Date().getFullYear() + ' iMaeGoo\n' +
13 | 'Released under the MIT License.\n' +
14 | 'Last Update: ' + (new Date()).toLocaleString()
15 |
16 | function getConfig ({ extractCss }) {
17 | return {
18 | module: {
19 | rules: [
20 | { test: /\.vue$/, loader: 'vue-loader' },
21 | extractCss
22 | ? { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] }
23 | : { test: /\.css$/, use: ['vue-style-loader', 'css-loader'] },
24 | { test: /\.svg$/, loader: 'svg-inline-loader' },
25 | { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: ['@babel/plugin-transform-modules-commonjs', '@babel/transform-runtime'] } } }
26 | ]
27 | },
28 | entry: extractCss
29 | ? {
30 | twikoo: './src/client/main.all.js'
31 | }
32 | : {
33 | twikoo: './src/client/main.js',
34 | 'twikoo.all': './src/client/main.all.js'
35 | },
36 | output: {
37 | path: BUILD_PATH,
38 | filename: extractCss ? '[name].nocss.js' : '[name].min.js',
39 | library: 'twikoo',
40 | libraryTarget: 'umd',
41 | globalObject: 'this'
42 | },
43 | target: ['web', 'es5'],
44 | plugins: [
45 | new webpack.BannerPlugin(banner),
46 | new CopyPlugin({
47 | patterns: [
48 | { from: 'demo/', to: './' }
49 | ]
50 | }),
51 | new VueLoaderPlugin(),
52 | new MiniCssExtractPlugin(),
53 | new TerserPlugin({
54 | parallel: 4,
55 | terserOptions: {
56 | ecma: 5,
57 | toplevel: true,
58 | ie8: true,
59 | safari10: true
60 | }
61 | })
62 | ],
63 | devServer: {
64 | static: [{
65 | directory: BUILD_PATH,
66 | publicPath: '/dist/',
67 | serveIndex: true,
68 | watch: true
69 | }],
70 | port: 9820,
71 | host: 'localhost',
72 | open: true,
73 | hot: true,
74 | compress: true
75 | },
76 | performance: {
77 | maxEntrypointSize: 524288,
78 | maxAssetSize: 524288
79 | }
80 | }
81 | }
82 |
83 | module.exports = process.env.NODE_ENV === 'production'
84 | ? [
85 | getConfig({ extractCss: false }),
86 | getConfig({ extractCss: true })
87 | ]
88 | : getConfig({ extractCss: false })
89 |
--------------------------------------------------------------------------------