├── .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 | Twikoo 2 | 3 | ---- 4 | 5 | [![](https://img.shields.io/npm/v/twikoo)](https://www.npmjs.com/package/twikoo) 6 | [![](https://img.shields.io/bundlephobia/minzip/twikoo)](https://bundlephobia.com/result?p=twikoo) 7 | [![](https://img.shields.io/npm/dt/twikoo)](https://www.npmjs.com/package/twikoo) 8 | [![](https://data.jsdelivr.com/v1/package/npm/twikoo/badge)](https://www.jsdelivr.com/package/npm/twikoo) 9 | [![](https://img.shields.io/npm/l/twikoo)](./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 | ![Comments](./docs/static/readme-1.png) 90 | 91 | ### Management 92 | 93 | ![Management](./docs/static/readme-2.png) 94 | 95 | ### Notification 96 | 97 | ![Notification](./docs/static/readme-3.jpg) 98 | 99 |
100 | 101 | ## Quick Start 102 | 103 | [![Deploy](https://main.qcloudimg.com/raw/67f5a389f1ac6f3b4d04c7256438e44f.svg)](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 | 1080829142 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 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimaegoo%2Ftwikoo.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fimaegoo%2Ftwikoo?ref=badge_large) 146 | 147 |
148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Twikoo 2 | 3 | ---- 4 | 5 | [![](https://img.shields.io/npm/v/twikoo)](https://www.npmjs.com/package/twikoo) 6 | [![](https://img.shields.io/bundlephobia/minzip/twikoo)](https://bundlephobia.com/result?p=twikoo) 7 | [![](https://img.shields.io/npm/dt/twikoo)](https://www.npmjs.com/package/twikoo) 8 | [![](https://data.jsdelivr.com/v1/package/npm/twikoo/badge)](https://www.jsdelivr.com/package/npm/twikoo) 9 | [![](https://img.shields.io/npm/l/twikoo)](./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 | ![评论](./docs/static/readme-1.png) 86 | 87 | ### 评论管理 88 | 89 | ![评论管理](./docs/static/readme-2.png) 90 | 91 | ### 推送通知 92 | 93 | ![推送通知](./docs/static/readme-3.jpg) 94 | 95 |
96 | 97 | ## 快速上手 | Quick Start 98 | 99 | 有关详细教程,请查看[快速上手](https://twikoo.js.org/quick-start.html) 100 | 101 |
102 | 如果你想获取更新动态、建言献策、参与测试,欢迎加入讨论群:1080829142 103 | 1080829142 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 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimaegoo%2Ftwikoo.svg?type=large)](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 |
47 | 62 |
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 |
106 |
107 |
108 |
109 |
110 |
111 |
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 | 20 | 21 | 50 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/Twikoo.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 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 | Twikoo 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 | ![Comments](../static/readme-1.png) 96 | 97 | ### Management 98 | 99 | ![Management](../static/readme-2.png) 100 | 101 | ### Notification 102 | 103 | ![Notification](../static/readme-3.jpg) 104 | 105 | 106 | 107 | ## Quick Start 108 | 109 | [![Deploy](https://main.qcloudimg.com/raw/67f5a389f1ac6f3b4d04c7256438e44f.svg)](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 | 1080829142 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 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimaegoo%2Ftwikoo.svg?type=large)](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 | ![](./static/faq-1.png) 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 | ![katex](./static/katex.png) 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 | ![](./static/vercel-1.png) 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 | Twikoo 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 | ![评论](./static/readme-1.png) 97 | 98 | ### 评论管理 99 | 100 | ![评论管理](./static/readme-2.png) 101 | 102 | ### 推送通知 103 | 104 | ![推送通知](./static/readme-3.jpg) 105 | 106 | ## 交流群 107 | 108 | 如果你想获取更新动态、建言献策、参与测试,欢迎加入讨论群:
109 | 1080829142 110 | 111 | ## 浏览器支持 112 | 113 | ::: tip 提示 114 | 技术原因,不兼容 IE 115 | ::: 116 | 117 | | IE / Edge
IE / Edge | Firefox
Firefox | Chrome
Chrome | Safari
Safari | iOS 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 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fimaegoo%2Ftwikoo.svg?type=large)](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 | ![](./static/mongodb-1.png) 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 | ![](./static/mongodb-2.png) 14 | 15 | 5. 在 Database 页面点击 Connect,连接方式选择 Drivers,并记录数据库连接字符串,请将连接字符串中的 `:` 修改为刚刚创建的数据库 `用户名:密码` 16 | 17 | ![](./static/mongodb-3.png) 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 ':'
 21 |         + text
 22 |         + ':'; 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 '' 70 | + text 71 | + '\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 + '\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 + '\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 = ''; 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 = '' + text + '' : '>'; 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 = `` + 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 | 8 | 9 | 33 | 34 | 127 | -------------------------------------------------------------------------------- /src/client/view/components/TkAction.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 56 | 57 | 94 | -------------------------------------------------------------------------------- /src/client/view/components/TkAdminExport.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 47 | -------------------------------------------------------------------------------- /src/client/view/components/TkAdminImport.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 107 | 108 | 125 | -------------------------------------------------------------------------------- /src/client/view/components/TkAvatar.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 68 | 69 | 99 | -------------------------------------------------------------------------------- /src/client/view/components/TkComments.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 123 | 124 | 192 | -------------------------------------------------------------------------------- /src/client/view/components/TkFooter.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 42 | 43 | 52 | -------------------------------------------------------------------------------- /src/client/view/components/TkMetaInput.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 153 | 154 | 186 | -------------------------------------------------------------------------------- /src/client/view/components/TkPagination.vue: -------------------------------------------------------------------------------- 1 | 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://img.shields.io/npm/v/twikoo-func)](https://www.npmjs.com/package/twikoo-func) 4 | [![](https://img.shields.io/npm/l/twikoo-func)](./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 | --------------------------------------------------------------------------------