Compare commits

...
84 Commits
Author SHA1 Message Date
github-actions[bot] 8af713636c chore: release v2.8.3 (#788)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-03-04 22:13:11 +08:00
qwqcode 5bf6e5f1dc fix(exception): missing message field after refactoring exception data structure 2024-03-04 21:04:38 +08:00
qwqcode 929faf0722 fix(sidebar/login): cannot select a name with the same email upon login (#781) 2024-03-04 21:01:04 +08:00
qwqcode 5322f90383 fix(captcha/image): unable to display image captcha on initial load 2024-03-04 20:56:31 +08:00
qwqcode fa3575cbae fix(conf): skip patching IP region conf if disabled 2024-03-04 20:05:20 +08:00
qwqcode c94a34ffd7 feat(comment): add is_verified field for comment entity 2024-02-16 19:43:35 +08:00
qwqcode 50b49448b6 feat(app): add onConfUpdated hook 2024-02-15 23:02:33 +08:00
qwqcode 8ff3f5e875 refactor(dao): validate the args of the FindCreateUser function 2024-02-14 00:21:48 +08:00
qwqcode 640fa7ded4 feat(ui/plugins): support dynamic loading of ui plug-in scripts (#774) 2024-02-12 21:12:55 +08:00
qwqcode 18123f7002 refactor(origin): extract CheckURLTrusted function 2024-02-12 20:36:43 +08:00
qwqcode cf2094667d feat(api/domain): add GET /conf/domain endpoint 2024-02-11 23:47:04 +08:00
qwqcode 2cd2165f06 refactor(entity/user): extract CheckPassword function 2024-02-11 23:36:19 +08:00
qwqcode 4eada4b628 feat(api/auth): invalidate old auth tokens after changing password 2024-02-11 23:34:32 +08:00
qwqcode 05f3ad043a feat(ui/editor): add beforeSubmit hook for editor submitting 2024-02-11 23:11:04 +08:00
qwqcode 6e9083e75d feat(ui/api): add ctx.getApiHandlers() to handle error responses and expose hooks 2024-02-11 23:00:22 +08:00
qwqcode 7bbdb5900f fix(ui/layer): ensure immediate operation when calling show() after hide() 2024-02-11 22:36:18 +08:00
qwqcode 5be45315dc chore(style/icon): standardized icon css styles 2024-02-09 16:59:58 +08:00
qwqcode 5d20ff1ef5 chore(fiber_logger): correct the http status code range for err log 2024-02-06 16:03:04 +08:00
qwqcode 86e0eeafd1 perf(ui/editor): improve the icon details for editor 2024-02-05 21:49:04 +08:00
github-actions[bot] 49c1a87a67 chore: release v2.8.2 (#769)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-05 19:09:15 +08:00
qwqcode ec7f81cb83 chore(docs): import artalk css via node 2024-02-05 19:01:39 +08:00
qwqcode 4b115d7b6a perf(ui/editor): optimize editor styles for narrow-width screens 2024-02-05 17:19:53 +08:00
qwqcode 1bae6d8d2a perf(ui/editor): modify editor plug-in text buttons to icons for brevity 2024-02-05 17:19:53 +08:00
qwqcode bdafa19470 perf(ui/editor): place Cancel Reply at the bottom of editor area (#644) 2024-02-05 17:19:40 +08:00
qwqcode 342236b6a7 perf(ui/lite): disable ArtalkLite preview config option by default 2024-02-05 17:05:00 +08:00
qwqcode ffce9f9e85 fix(style): replace style break-all with break-word in content el (#644) 2024-02-05 15:05:58 +08:00
qwqcode 0097e041b1 fix(api/comments): ua field is optional and not required (#767) 2024-02-05 14:30:17 +08:00
github-actions[bot] 42da621dfc chore: release v2.8.1 (#766)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-04 22:21:43 +08:00
qwqcode 2f5dbaa022 chore(i18n): update zh-CN 2024-02-04 22:02:13 +08:00
qwqcode e181a6d5dd perf(style/sidebar): improve some detail in sidebar 2024-02-04 21:55:59 +08:00
qwqcode 7873b21fb7 fix(ui/scroll): scroll into view issue when relativeTo parameter is provided 2024-02-04 21:53:02 +08:00
qwqcode 2a219518a4 feat(ui/goto): improved solution of navigating to comment via link hash (#693) (#765) 2024-02-04 21:47:06 +08:00
qwqcode afa7888db0 chore(typo): fix comment typo 2024-02-04 20:26:34 +08:00
qwqcode f6a589d5b2 refactor(ui/list): refactor the list layout manager 2024-02-04 20:25:21 +08:00
qwqcode 41a09d5996 refactor(ui/comment): loose coupling between Comment and Context 2024-02-04 20:15:28 +08:00
qwqcode 191977c0df feat(api): add GET /comments/:id endpoint (#764) 2024-02-04 20:08:38 +08:00
qwqcode 5a4c06daee chore(ip_region): skip empty string 2024-02-04 19:48:26 +08:00
qwqcode c4942ab807 chore(Makefile): add http-client build cmd to update-swagger 2024-02-04 03:24:58 +08:00
qwqcode 6b83a9f632 chore(transfer): remove the url_resolver disabled warning msg 2024-02-04 02:25:33 +08:00
qwqcode 3c88c0c29c docs: supplement and refine fe-api.md 2024-02-04 02:22:49 +08:00
qwqcode 35aab6bedc fix(ui/list): unable to reply or delete comments after switching pagination 2024-02-04 01:40:07 +08:00
qwqcode 17033dc111 fix(ui/no_comment): always appears when creating comment 2024-02-03 23:45:48 +08:00
qwqcode 559df60f0d fix(ui/pv): missing pageTitle field in the PV request 2024-02-03 23:12:12 +08:00
qwqcode 22d4ec1c31 chore(docs): rename IP region in zh-CN 2024-02-03 21:44:37 +08:00
qwqcode d97ad5c843 fix(ip_region): unable to output the IP region correctly (#760) 2024-02-03 21:43:42 +08:00
qwqcode 34a67f647b chore(docs): update cdn source links 2024-02-03 19:58:38 +08:00
qwqcode 9c3228b8fc feat(config): add db.prepare_stmt config option (#760) (#762)
The Prepared Statement database feature is enabled by default. You can disable it in config file or a env variable `ATK_DB_PREPARE__STMT=0`.
2024-02-03 19:19:00 +08:00
qwqcode 4012d3e090 fix(cmd/admin): update user role to admin if already exists 2024-02-03 16:54:13 +08:00
qwqcode bc1c4d315c fix(api/comments): counting comments issue in PostgreSQL db (#760) (#761) 2024-02-03 15:34:25 +08:00
qwqcode 2a6f0c56a4 chore(docs): fix script cdn links 2024-02-01 21:43:52 +08:00
github-actions[bot] 46e2a15116 chore: release v2.8.0 (#756)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-02-01 00:19:18 +08:00
qwqcode c068f5a240 chore(deps): bump golang deps 2024-02-01 00:11:34 +08:00
qwqcode 29bf49958f perf(ui/pagination): replace the prev/next text on pagination buttons with icons (#743) 2024-01-31 23:59:58 +08:00
qwqcode 2fcad539ca perf(ui/i18n): refine some translation (#743) 2024-01-31 23:28:56 +08:00
qwqcode 45b79124c9 chore(ui/deps): bump deps for ui 2024-01-31 20:27:14 +08:00
qwqcode 2b44452234 refactor(ui): optimize init and update lifecycle events 2024-01-31 19:44:40 +08:00
qwqcode 0e0b53fe9e perf(ui/sidebar): prevent potential token leakage by clearing query in iframe url
The previous referrer policy was already set to `strict-origin`, but to further enhance security, this modification was made.
2024-01-30 23:07:37 +08:00
qwqcode dba5ba5277 fix(ui/no_comment): flickering when clearing list before fetch 2024-01-30 22:24:53 +08:00
qwqcode 942aaf654f fix(ui/sidebar): incorrectly increasing pv count in sidebar (#724) 2024-01-30 20:27:19 +08:00
qwqcode 37e0091502 perf(ui/conf): support for watching partial changes in config with watchConf() (#755) 2024-01-30 19:14:09 +08:00
qwqcode a7c3f1bbda feat(notify): add notify_pending config option (#728) (#754) 2024-01-29 21:22:36 +08:00
qwqcode a40a070bdb chore(fix/pin): fix pinned comment sort rule 2024-01-29 14:46:19 +08:00
Frost Ming 082bfc63d1 fix(ui/editor): prevent propagation of click event in editor (#734)
Signed-off-by: Frost Ming <me@frostming.com>
2024-01-29 14:12:21 +08:00
qwqcode 0de8009e65 feat(ui/admin): add logout button in admin panel (#488) (#753)
fixes #488
2024-01-29 13:56:02 +08:00
qwqcode a5e4c81e72 fix(ui/sidebar_layer): sidebar still displays when login is required 2024-01-29 13:34:51 +08:00
qwqcode c52c26145c fix(ui/layer): unexpected behavior with multiple items when closing 2024-01-29 00:47:50 +08:00
qwqcode f5e310a182 chore(ui/ctx): remove token public in context 2024-01-28 22:14:24 +08:00
qwqcode 0828b91602 refactor(ui/comment): rename class Comment to CommentNode 2024-01-28 22:13:20 +08:00
qwqcode c4f664e412 refactor(ui/ctx): add getComments and getCommentNodes in context (#752)
* refactor(ui/ctx): add `getComments` and `getCommentNodes` func in context

* fix import
2024-01-28 22:03:15 +08:00
qwqcode a084c50095 refactor(transfer): normalize transfer payload parameter name 2024-01-28 13:33:03 +08:00
qwqcode 9dacd0954d fix(cache): unable to change admin list after caching (#748) (#750) 2024-01-26 22:27:10 +08:00
qwqcode 50b0a6068c chore(ci): fix ui test trigger rules 2024-01-26 17:16:18 +08:00
qwqcode 4689492bf5 fix(ui/pagination): auto load list when scroll reaches bottom 2024-01-26 17:09:56 +08:00
qwqcode 6ee9ee9ea4 fix(ui/lite): correct the instance name for compiling ArtalkLite (#740) 2024-01-26 11:45:22 +08:00
qwqcode 8e3a743ed8 fix(i18n): sidebar language cannot be changed
Also follow 0d35e66
2024-01-26 11:42:39 +08:00
qwqcode 9550a16a0c chore(fetch): returns empty object when json deserialize failed 2024-01-25 01:07:48 +08:00
qwqcode a9edac3ce0 refactor(api): update swagger file of API v2
BREAKING CHANGE: For Developers: The HTTP API has now been updated to version v2, with a lot of adjustments made, such as changing to Restful API style, request type, request body, response body, etc. data structure, adding error response codes. You can refer to #735 or https://artalk.js.org/http-api Please update all your existing clients to adapt to the new v2 server. The new base path is `/api/v2/`. The UI package's HTTP client has been implemented to auto-generate based on `Swagger.json`, and there have been a lot of adjustments to the `artalk.ctx.getApi()` related code. In addition, the HTTP API no longer enforces checks on the header's `Origin` and `Referer` (to implement strict isolation of different sites) but still maintains the original CORS origin check.

For General Users: Now all administrators have the same level of authority (there is no longer super admin authority), the function of assigning sub-administrators with independent scope authority to individual sites has been removed, which can simplify the HTTP server maintenance and implementation. Upgrading only requires updating the client and server application at the same time, and there are no additional operations.
2024-01-24 21:53:34 +08:00
qwqcode 589850d311 refactor(api): refactor HTTP API to version 2 (#735)
* refactor(api): refactor HTTP API to version 2

* refactor: remove forced site_origin check

* refactor: update http client

* fix

* add more api description and error code

* reduce nestings in response structures

* separate unread

* refactor(api/comments): get comments

* refactor: frontend api module

* chore: remove vite temporary file

* chore: restore swagger file

* update

* 移除为管理员分配站点名的站点隔离功能,简化实现

* refactor pagesAllFetch

* fix ci

* remove useless codes

* limiter protected paths

* add test for version endpoint

* rename handler code files

* code file separate

* fix some bug of interacting with backend

* add fetch error class for handling

* fix and rename

* refactor: checker support both `Promise` and callback fn

* fix new checker captcha iframe url

* rename and add swagger id field

* replace tab with spaces in go comment

* chore: refine

* complete the validate tag for request fields and enable `--defaultRequired`

90% of the request fields of our API are required,100% of response fields in our API are required.
Each field add validate:"required" is easy to miss.

* replace original api client with auto-generated codes

* artalk pkg apply new api client

* artalk-sidebar pkg apply new api client

* fix issues

* fix sidebar locale conf and refactor user login

* fix and refactor captcha and limiter

* fix ui testing

* add script to build http client by swagger.json
2024-01-24 21:33:30 +08:00
qwqcode 9ba9439831 chore(typo): fix typo for login in zh 2024-01-24 21:14:50 +08:00
qwqcode 38f6b6996a docs: update multi-sites manual (#747) 2024-01-24 21:01:56 +08:00
qwqcode 1e16570b2f chore(ci): fix ui test trigger rules 2024-01-24 16:24:36 +08:00
qwqcode 889adc6bae chore(docs): update security.md 2023-12-31 23:46:34 +08:00
qwqcode 0de59911b3 chore(docs): fix broken links 2023-12-29 12:40:04 +08:00
qwqcode f22e557b1c feat(go): upgrade golang to v1.21.5 and some deps 2023-12-28 23:26:56 +08:00
318 changed files with 20879 additions and 15349 deletions

No files matched your search

+3 -3
View File
@@ -13,7 +13,7 @@ executors:
- image: 'cimg/node:18.19.0'
go:
docker:
- image: cimg/go:1.21.3
- image: cimg/go:1.21.5
parameters:
trigger-app:
@@ -38,12 +38,12 @@ jobs:
go version
- restore_cache:
keys:
- go-mod-1.21.3-{{ checksum "go.sum" }}
- go-mod-1.21.5-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: go mod download
- save_cache:
key: go-mod-1.21.3-{{ checksum "go.sum" }}
key: go-mod-1.21.5-{{ checksum "go.sum" }}
paths:
- "~/go/pkg/mod"
- run:
+1 -1
View File
@@ -11,7 +11,7 @@ on:
type: boolean
env:
GO_VERSION: 1.21.3
GO_VERSION: 1.21.5
PKG_NAME: github.com/ArtalkJS/Artalk
DOCKER_IMG: ghcr.io/goreleaser/goreleaser-cross
CACHE_DIR: /tmp/cache/docker-image
+1 -1
View File
@@ -54,7 +54,7 @@ jobs:
# since docker push actions will override the previous images,
# so need to build `linux/amd64` and push first,
# then build `linux/amd64` twice with other archs (build very slow so keep it last).
target: ["linux/amd64", "linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6"]
target: ["linux/amd64", "linux/amd64,linux/arm64,linux/arm/v7"]
with:
version: ${{ needs.tagging.outputs.version }}
dry_run: ${{ inputs.dry_run || false }}
+3 -7
View File
@@ -2,15 +2,11 @@ name: Docs CN Mirror Deploy
on:
push:
branches:
- master
paths:
- "docs/**"
branches: ['master']
paths: ['docs/**']
jobs:
deploy:
if: github.repository == 'ArtalkJS/Artalk'
runs-on: ubuntu-latest
steps:
@@ -39,6 +35,6 @@ jobs:
secret_key: ${{ secrets.TENCENT_CLOUD_SECRET_KEY }}
cos_bucket: ${{ secrets.DOCS_CN_COS_BUCKET }}
cos_region: ${{ secrets.DOCS_CN_COS_REGION }}
local_path: .vitepress/dist/
local_path: docs/docs/.vitepress/dist/
remote_path: /
clean: true
+3 -1
View File
@@ -2,10 +2,12 @@ name: Test Docs
on:
push:
branches: ['*', '!release/*']
paths: ['docs/**']
tags-ignore: ['v*']
pull_request:
paths: ['docs/**']
branches: ['!release/*']
branches: ['*', '!release/*']
jobs:
test_docs:
+3 -1
View File
@@ -2,10 +2,12 @@ name: Test Frontend
on:
push:
branches: ['*', '!release/*']
paths: ['ui/**']
tags-ignore: ['v*']
pull_request:
paths: ['ui/**']
branches: ['!release/*']
branches: ['*', '!release/*']
jobs:
test_ui:
+126
View File
@@ -1,4 +1,130 @@
## [v2.8.3](https://github.com/ArtalkJS/Artalk/compare/v2.8.2...v2.8.3) (2024-03-04)
### Features
* **api/auth:** invalidate old auth tokens after changing password
* **api/domain:** add GET `/conf/domain` endpoint
* **app:** add `onConfUpdated` hook
* **comment:** add `is_verified` field for comment entity
* **ui/api:** add `ctx.getApiHandlers()` to handle error responses and expose hooks
* **ui/editor:** add `beforeSubmit` hook for editor submitting
* **ui/plugins:** support dynamic loading of ui plug-in scripts ([#774](https://github.com/ArtalkJS/Artalk/issues/774))
### Bug Fixes
* **captcha/image:** unable to display image captcha on initial load
* **conf:** skip patching IP region conf if disabled
* **exception:** missing `message` field after refactoring exception data structure
* **sidebar/login:** cannot select a name with the same email upon login ([#781](https://github.com/ArtalkJS/Artalk/issues/781))
* **ui/layer:** ensure immediate operation when calling `show()` after `hide()`
### Performance Improvements
* **ui/editor:** improve the icon details for editor
### Code Refactoring
* **dao:** validate the args of the `FindCreateUser` function
* **entity/user:** extract `CheckPassword` function
* **origin:** extract `CheckURLTrusted` function
## [v2.8.2](https://github.com/ArtalkJS/Artalk/compare/v2.8.1...v2.8.2) (2024-02-05)
### Bug Fixes
* **api/comments:** `ua` field is optional and not required ([#767](https://github.com/ArtalkJS/Artalk/issues/767))
* **style:** replace style `break-all` with `break-word` in content el ([#644](https://github.com/ArtalkJS/Artalk/issues/644))
### Performance Improvements
* **ui/editor:** optimize editor styles for narrow-width screens
* **ui/editor:** modify editor plug-in text buttons to icons for brevity
* **ui/editor:** place `Cancel Reply` at the bottom of editor area ([#644](https://github.com/ArtalkJS/Artalk/issues/644))
* **ui/lite:** disable ArtalkLite `preview` config option by default
## [v2.8.1](https://github.com/ArtalkJS/Artalk/compare/v2.8.0...v2.8.1) (2024-02-04)
### Features
* **api:** add GET `/comments/:id` endpoint ([#764](https://github.com/ArtalkJS/Artalk/issues/764))
* **config:** add `db.prepare_stmt` config option ([#760](https://github.com/ArtalkJS/Artalk/issues/760)) ([#762](https://github.com/ArtalkJS/Artalk/issues/762))
* **ui/goto:** improved solution of navigating to comment via link hash ([#693](https://github.com/ArtalkJS/Artalk/issues/693)) ([#765](https://github.com/ArtalkJS/Artalk/issues/765))
### Bug Fixes
* **api/comments:** counting comments issue in PostgreSQL db ([#760](https://github.com/ArtalkJS/Artalk/issues/760)) ([#761](https://github.com/ArtalkJS/Artalk/issues/761))
* **cmd/admin:** update user role to admin if already exists
* **ip_region:** unable to output the IP region correctly ([#760](https://github.com/ArtalkJS/Artalk/issues/760))
* **ui/list:** unable to reply or delete comments after switching pagination
* **ui/no_comment:** always appears when creating comment
* **ui/pv:** missing `pageTitle` field in the PV request
* **ui/scroll:** scroll into view issue when `relativeTo` parameter is provided
### Performance Improvements
* **style/sidebar:** improve some detail in sidebar
### Code Refactoring
* **ui/comment:** loose coupling between `Comment` and `Context`
* **ui/list:** refactor the list layout manager
### Documentation
* supplement and refine `fe-api.md`
## [v2.8.0](https://github.com/ArtalkJS/Artalk/compare/v2.7.3...v2.8.0) (2024-01-31)
### Features
* **go:** upgrade golang to v1.21.5 and some deps
* **notify:** add `notify_pending` config option ([#728](https://github.com/ArtalkJS/Artalk/issues/728)) ([#754](https://github.com/ArtalkJS/Artalk/issues/754))
* **ui/admin:** add logout button in admin panel ([#488](https://github.com/ArtalkJS/Artalk/issues/488)) ([#753](https://github.com/ArtalkJS/Artalk/issues/753))
### Bug Fixes
* **cache:** unable to change admin list after caching ([#748](https://github.com/ArtalkJS/Artalk/issues/748)) ([#750](https://github.com/ArtalkJS/Artalk/issues/750))
* **i18n:** sidebar language cannot be changed
* **ui/editor:** prevent propagation of click event in editor ([#734](https://github.com/ArtalkJS/Artalk/issues/734))
* **ui/layer:** unexpected behavior with multiple items when closing
* **ui/lite:** correct the instance name for compiling ArtalkLite ([#740](https://github.com/ArtalkJS/Artalk/issues/740))
* **ui/no_comment:** flickering when clearing list before fetch
* **ui/pagination:** auto load list when scroll reaches bottom
* **ui/sidebar:** incorrectly increasing pv count in sidebar ([#724](https://github.com/ArtalkJS/Artalk/issues/724))
* **ui/sidebar_layer:** sidebar still displays when login is required
### Performance Improvements
* **ui/conf:** support for watching partial changes in config with `watchConf()` ([#755](https://github.com/ArtalkJS/Artalk/issues/755))
* **ui/i18n:** refine some translation ([#743](https://github.com/ArtalkJS/Artalk/issues/743))
* **ui/pagination:** replace the prev/next text on pagination buttons with icons ([#743](https://github.com/ArtalkJS/Artalk/issues/743))
* **ui/sidebar:** prevent potential token leakage by clearing query in iframe url
### Code Refactoring
* **api:** update swagger file of API v2
* **api:** refactor HTTP API to version 2 ([#735](https://github.com/ArtalkJS/Artalk/issues/735))
* **transfer:** normalize transfer payload parameter name
* **ui:** optimize init and update lifecycle events
* **ui/comment:** rename class `Comment` to `CommentNode`
* **ui/ctx:** add `getComments` and `getCommentNodes` in context ([#752](https://github.com/ArtalkJS/Artalk/issues/752))
### Documentation
* update multi-sites manual ([#747](https://github.com/ArtalkJS/Artalk/issues/747))
### BREAKING CHANGE
For Developers: The HTTP API has now been updated to version v2, with a lot of adjustments made, such as changing to Restful API style, request type, request body, response body, etc. data structure, adding error response codes. You can refer to [#735](https://github.com/ArtalkJS/Artalk/issues/735) or https://artalk.js.org/http-api Please update all your existing clients to adapt to the new v2 server. The new base path is `/api/v2/`. The UI package's HTTP client has been implemented to auto-generate based on `Swagger.json`, and there have been a lot of adjustments to the `artalk.ctx.getApi()` related code. In addition, the HTTP API no longer enforces checks on the header's `Origin` and `Referer` (to implement strict isolation of different sites) but still maintains the original CORS origin check.
For General Users: Now all administrators have the same level of authority (there is no longer super admin authority), the function of assigning sub-administrators with independent scope authority to individual sites has been removed, which can simplify the HTTP server maintenance and implementation. Upgrading only requires updating the client and server application at the same time, and there are no additional operations.
## [v2.7.3](https://github.com/ArtalkJS/Artalk/compare/v2.7.2...v2.7.3) (2023-12-28)
### Features
+2 -2
View File
@@ -1,5 +1,5 @@
### build Artalk
FROM golang:1.21.3-alpine3.18 as builder
FROM golang:1.21.5-alpine3.19 as builder
WORKDIR /source
@@ -40,7 +40,7 @@ RUN set -ex \
&& make build
### build final image
FROM alpine:3.18
FROM alpine:3.19
# we set the timezone `Asia/Shanghai` by default, you can be modified
# by `docker build --build-arg="TZ=Other_Timezone ..."`
+2 -1
View File
@@ -52,7 +52,8 @@ update-i18n:
update-swagger:
go install github.com/swaggo/swag/cmd/swag@latest
swag init -g server/server.go --output ./docs/swagger
swag init -g server/server.go --output ./docs/swagger --requiredByDefault
pnpm -r swagger:build-http-client
docker-build:
./scripts/docker-build.sh
+5 -4
View File
@@ -69,9 +69,10 @@ npm install artalk
import Artalk from 'artalk'
Artalk.init({
el: '#Comments',
site: 'Artalk 的博客',
server: 'https://artalk.example.com'
el: '#Comments',
site: 'Artalk 的博客',
server: 'https://artalk.example.com',
pageKey: '/2018/10/02/hello-world.html'
})
```
@@ -202,7 +203,7 @@ see [CONTRIBUTING.md](./CONTRIBUTING.md)
- [x] 用户鉴权机制
- [x] 跨域非法请求阻止
- [x] 全局验证码操作次数限制
- [x] JWT 登陆状态验证
- [x] JWT 登录状态验证
- [x] 时区自定义
- [x] 只看作者功能
- [x] 图片上传
+1
View File
@@ -41,6 +41,7 @@ func NewAdminCommand(app *ArtalkCmd) *cobra.Command {
findUser := app.Dao().FindUser(username, email)
if !findUser.IsEmpty() {
findUser.SetPasswordEncrypt(password)
findUser.IsAdmin = true
if err := app.Dao().UpdateUser(&findUser); err != nil {
log.Fatal(err)
}
-2
View File
@@ -30,8 +30,6 @@ func NewImportCommand(app *ArtalkCmd) *cobra.Command {
params := artransfer.ArrToImportParams(payload)
params.Assumeyes, _ = cmd.Flags().GetBool("assumeyes")
artransfer.RunImportArtrans(app.Dao(), params)
log.Info(i18n.T("Import complete"))
},
}
+3
View File
@@ -17,6 +17,7 @@ db:
password: ""
charset: "utf8mb4"
ssl: false
prepare_stmt: true
log:
enabled: true
filename: "./data/artalk.log"
@@ -102,6 +103,7 @@ email:
account_name: "noreply@example.com"
admin_notify:
notify_tpl: "default"
notify_pending: false
noise_mode: false
email:
enabled: true
@@ -166,3 +168,4 @@ frontend:
scrollable: false
reqTimeout: 15000
versionCheck: true
pluginURLs: []
+7 -1
View File
@@ -47,6 +47,8 @@ db:
charset: "utf8mb4"
# Enable SSL mode
ssl: false
# Prepared Statement
prepare_stmt: true
# Logging
log:
@@ -214,10 +216,12 @@ email:
access_key_secret: ""
account_name: "noreply@example.com"
# Multi-push
# Multi-Push
admin_notify:
# Notification template (set to file path to use custom template)
notify_tpl: "default"
# Pending comment still send notification (notifications are still sent when comments are intercepted)
notify_pending: false
# Noise mode
# -- noise_mode is disabled by default. --
# -- When this option is set to `false`, only messages sent to the administrator will be notified, --
@@ -325,3 +329,5 @@ frontend:
reqTimeout: 15000
# Version check
versionCheck: true
# Plugins
pluginURLs: []
+10 -4
View File
@@ -19,7 +19,7 @@ timezone: "Asia/Shanghai"
# 默认站点名
site_default: "默认站点"
# 登陆有效时长 (单位:秒)
# 登录有效时长 (单位:秒)
login_timeout: 259200
# 数据库
@@ -44,6 +44,8 @@ db:
table_prefix: ""
# 启用 SSL
ssl: false
# 预编译语句
prepare_stmt: true
# 日志
log:
@@ -162,9 +164,9 @@ captcha:
captcha_id: ""
captcha_key: ""
# IP 归属地
# IP 属地
ip_region:
# 启用 IP 归属地展示
# 启用 IP 属地展示
enabled: false
# 数据文件路径 (.xdb 格式)
db_path: "./data/ip2region.xdb"
@@ -226,6 +228,8 @@ email:
admin_notify:
# 通知模版 (填入文件路径使用自定义模板)
notify_tpl: "default"
# 待审评论仍然发送通知 (当评论被拦截时仍然发送通知)
notify_pending: false
# 嘈杂模式
noise_mode: false
# 邮件通知管理员
@@ -274,7 +278,7 @@ admin_notify:
- "USER_ID_1"
- "GROUP_ID_1"
# 前端配置
# 界面配置
frontend:
# 评论框占位文字
placeholder: ""
@@ -332,3 +336,5 @@ frontend:
reqTimeout: 15000
# 版本检测
versionCheck: true
# 插件
pluginURLs: []
+14 -14
View File
@@ -19,29 +19,29 @@ export default defineConfig({
],
// artalk
// ['link', { href: `https://npm.elemecdn.com/artalk@${Version.latest}/dist/Artalk.css`, rel: 'stylesheet' }],
[
"link",
{ href: `https://artalk.qwqaq.com/dist/Artalk.css`, rel: "stylesheet" },
],
// [
// "link",
// { href: `https://artalk.qwqaq.com/dist/Artalk.css`, rel: "stylesheet" },
// ],
// ['script', { src: ArtalkCDN.JS }],
// light gallery
[
"link",
{
href: "https://npm.elemecdn.com/lightgallery@2.3.0/css/lightgallery.css",
href: "https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.3.0/css/lightgallery.css",
rel: "stylesheet",
},
],
[
"script",
{
src: "https://npm.elemecdn.com/lightgallery@2.3.0/lightgallery.min.js",
src: "https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.3.0/lightgallery.min.js",
},
],
// katex
// ['link', { href: "https://npm.elemecdn.com/katex@0.15.3/dist/katex.min.css", rel: 'stylesheet' }],
// ['script', { src: 'https://npm.elemecdn.com/katex@0.15.3/dist/katex.min.js' }],
// ['script', { src: 'https://npm.elemecdn.com/@artalkjs/plugin-katex/dist/artalk-plugin-katex.js' }],
['link', { href: "https://unpkg.com/katex@0.15.3/dist/katex.min.css", rel: 'stylesheet' }],
['script', { src: 'https://unpkg.com/katex@0.15.3/dist/katex.min.js' }],
// ['script', { src: 'https://unpkg.com/@artalk/plugin-katex/dist/artalk-plugin-katex.js' }],
],
lastUpdated: true,
@@ -118,7 +118,7 @@ export default defineConfig({
{ text: "浏览量统计", link: "/guide/frontend/pv.md" },
{ text: "Latex", link: "/guide/frontend/latex.md" },
{ text: "图片灯箱", link: "/guide/frontend/lightbox.md" },
{ text: "IP 归属地", link: "/guide/frontend/ip-region.md" },
{ text: "IP 属地", link: "/guide/frontend/ip-region.md" },
{ text: "精简版本", link: "/guide/frontend/artalk-lite.md" },
{ text: "置入博客", link: "/guide/frontend/import-blog.md" },
{ text: "置入框架", link: "/guide/frontend/import-framework.md" },
@@ -215,10 +215,10 @@ export default defineConfig({
text: "代码仓库",
link: "https://github.com/ArtalkJS/Artalk",
},
{
text: "文档镜像 (国内)",
link: "https://artalk-docs.qwqaq.com",
},
// {
// text: "文档镜像 (国内)",
// link: "https://artalk-docs.qwqaq.com",
// },
],
},
],
+1
View File
@@ -6,6 +6,7 @@
import { watch, nextTick, ref, onMounted, onUnmounted } from 'vue'
import { useData, useRouter } from 'vitepress'
import Artalk from 'artalk'
import 'artalk/dist/Artalk.css'
const el = ref<HTMLElement | null>(null)
+1 -1
View File
@@ -1,3 +1,3 @@
{
"latest": "2.7.3"
"latest": "2.8.3"
}
+8 -9
View File
@@ -4,33 +4,32 @@
|事件名|描述|
|-|-|
| `inited` | 初始化后 |
| `destroy` | 实例销毁时 |
| `created` | 初始化后 |
| `mounted` | 数据加载后 |
| `updated` | 数据更新后 |
| `unmounted` | 销毁后 |
| `list-fetch` | 评论列表请求时 |
| `list-fetched` | 评论列表请求后 |
| `list-load` | 评论装载前 |
| `list-loaded` | 评论装载后 |
| `list-error` | 评论加载错误时 |
| `list-failed` | 评论加载错误时 |
| `list-goto-first` | 评论列表归位时 |
| `list-reach-bottom` | 评论列表滚动到底部时 |
| `list-reach-bottom` | 评论列表滚动到底部时 |
| `comment-inserted` | 评论插入后 |
| `comment-updated` | 评论更新后 |
| `comment-deleted` | 评论删除后 |
| `comment-rendered` | 评论节点渲染后 |
| `unreads-updated` | 未读消息变更时 |
| `notifies-updated` | 未读消息变更时 |
| `list-goto` | 评论跳转时 |
| `page-loaded` | 页面数据更新后 |
| `editor-submit` | 编辑器提交时 |
| `editor-submitted` | 编辑器提交后 |
| `user-changed` | 本地用户数据变更时 |
| `conf-loaded` | 配置变更时 |
| `dark-mode-changed` | 深色模式变更时 |
| `sidebar-show` | 侧边栏显示 |
| `sidebar-hide` | 侧边栏隐藏 |
事件声明代码:[@ArtalkJS/Artalk - types/event.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/types/event.ts)
事件声明代码:[@ArtalkJS/Artalk - src/types/event.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/src/types/event.ts)
## 添加事件监听
+21 -9
View File
@@ -26,14 +26,16 @@ const artalk = Artalk.init({
})
```
调用该函数会向后端发起请求:
调用该函数会异步地向后端发起请求:
1. 获取前端配置
2. 获取评论列表
配置和评论加载完毕后会分别触发 `conf-loaded` 和 `list-loaded` 事件。
配置和插件加载完毕后会触发 `mounted` 事件。
注:前端配置可能会被覆盖,详情见:[在后端控制前端](../guide/backend/fe-control.md)。
评论列表加载完毕后会触发 `list-loaded` 事件。
注:前端界面配置可能会被覆盖,详情见:[在后端控制前端](../guide/backend/fe-control.md)。
## 释放资源 `destroy`
@@ -45,7 +47,7 @@ const artalk = Artalk.init({ ... })
artalk.destroy()
```
释放资源将执行 Artalk 挂载的 DOM 节点删除,以及解除 Artalk 事件监听等操作。
调用该函数时,将触发 `unmounted` 事件。释放资源将执行 Artalk 挂载的 DOM 节点删除,以及解除 Artalk 事件监听等操作。当该函数执行完毕后,Artalk 实例将不再可用。
在 Vue / React 等框架中,务必在组件销毁时调用该函数,否则会造成内存泄漏:
@@ -69,11 +71,11 @@ artalk.update({
})
```
调用该函数将触发 `conf-loaded` 事件。
调用该函数将触发 `updated` 事件。
更新配置不会自动刷新评论列表,可按需继续执行 `artalk.reload()` 函数。
请注意,该函数是一个实例化对象中的方法,并非全局函数。
请注意,该函数是一个实例化对象中的成员方法,并非全局函数。
前端配置参考:[前端配置](../guide/frontend/config.md)
@@ -96,8 +98,9 @@ artalk.reload()
```js
const artalk = Artalk.init({ ... })
artalk.on('list-loaded', () => {
alert('评论列表加载完毕')
artalk.on('list-loaded', (comments) => {
// comments 包含当前页面所有评论数据 (而非仅本次请求获取的部分评论)
console.log('评论列表加载完毕: ', comments)
})
```
@@ -125,7 +128,7 @@ artalk.off('list-loaded', handler)
```js
const artalk = Artalk.init({ ... })
artalk.trigger('list-loaded')
artalk.trigger('list-loaded', [...])
```
## 加载插件 `use`
@@ -154,6 +157,15 @@ const artalk = Artalk.init({ ... })
artalk.setDarkMode(true)
```
也可以通过调用 `Artalk.init` 时,通过 `darkMode` 参数设置初始夜间模式。
```js
const artalk = Artalk.init({
// ...其他配置
darkMode: 'auto',
})
```
## 浏览量组件 `loadCountWidget`
详情见:[浏览量统计](../guide/frontend/pv.md)
+1 -1
View File
@@ -23,6 +23,6 @@
* [@ArtalkJS/Artalk - server/server.go](https://github.com/ArtalkJS/Artalk/blob/master/server/server.go)
- `UI` 及 `Event` 部分参考源码
* [@ArtalkJS/Artalk - src/artalk.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/src/artalk.ts)
* [@ArtalkJS/Artalk - types/event.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/types/event.ts)
* [@ArtalkJS/Artalk - src/types/event.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/src/types/event.ts)
通过 Artalk 提供的 `API` 和 `Event`,你可以实现很多高级功能,比如编写评论管理机器人、评论提醒推送插件等。Artalk 并不为此提供技术指导,但鼓励你参考此处的文档自行定制。
+39 -13
View File
@@ -6,12 +6,26 @@
import Artalk from 'artalk'
Artalk.use((ctx) => {
ctx.on('inited', () => {
ctx.get('editor').setContent("Hello World")
})
ctx.on('list-loaded', () => {
console.log('评论列表加载完毕')
ctx.getCommentNodes().forEach((node) => {
node.getEl().style.background = 'red'
})
})
})
Artalk.use((ctx) => {
let el = null
ctx.on('mounted', () => {
el = document.createElement('div')
document.body.appendChild(el)
console.log('Artalk 实例被挂载')
})
ctx.on('unmounted', () => {
el.remove()
console.log('Artalk 实例被销毁')
})
})
@@ -20,11 +34,14 @@ const artalk = Artalk.init({ ... })
请注意:
- `Artalk.use` 必须在 `Artalk.init` 之前调用。
- `Artalk.use` 必须在 `Artalk.init` 之前调用才生效。
- 请勿依赖插件的加载顺序,请监听事件来执行插件逻辑。
- 当所有的插件加载完毕后,会触发 `inited` 事件。
- 当所有的插件加载完毕后,会触发 `created` 事件。
- 当插件和配置加载完毕后,会触发 `mounted` 事件。
- 当 Artalk 实例被销毁时,会触发 `unmounted` 事件。
- 当配置发生变化时,会触发 `updated` 事件。
在 use 函数中,你可以通过 `ctx` 来访问 Artalk 的上下文。
在 use 函数中,你可以通过 `ctx` 来访问 Artalk 的 Context 对象。
## Context
@@ -32,23 +49,32 @@ Context 对象包含了 Artalk 的上下文信息。
| 成员 | 说明 |
| --- | --- |
| `ctx.$root` | Artalk 容器元素 |
| `ctx.conf` | Artalk 配置 |
| `ctx.fetch` | 加载数据数据 |
| `ctx.getEl` | 获取容器元素 |
| `ctx.getConf` | 获取配置 |
| `ctx.updateConf` | 更新配置 |
| `ctx.watchConf` | 监听配置 |
| `ctx.setDarkMode` | 设置夜间模式 |
| `ctx.getApi` | 获取 API 客户端对象 |
|--|--|
| `ctx.fetch` | 获取评论数据 |
| `ctx.reload` | 重载评论列表 |
| `ctx.getComments` | 获取所有评论数据对象 |
| `ctx.getCommentNodes` | 获取所有评论节点对象 |
|--|--|
| `ctx.on` | 添加事件监听 |
| `ctx.off` | 解除事件监听 |
| `ctx.trigger` | 触发事件 |
| `ctx.inject` | 依赖注入 |
|--|--|
| `ctx.get` | 获取依赖 |
| `ctx.inject` | 注入依赖 |
::: warning
Context API 目前仍不稳定,开发可能会有变动,升级请关注 CHANGELOG。
:::
参考:[@artalk/types/context.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/types/context.ts)
参考:[@artalk/src/types/context.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/src/types/context.ts)
Artalk 拥有完整的 TypeScript 类型定义,你可以通过编辑器的自动补全来查看 API。
Artalk 包含完整的 TypeScript 类型定义,你可以通过编辑器的自动补全来查看 API。
## 示例插件
+10 -1
View File
@@ -229,6 +229,15 @@ admin_notify:
可用变量和邮件模板相同,可参考:[邮件模版](./email.md#邮件模板)
## 待审评论仍然发送通知 `notify_pending`
```yaml
admin_notify:
notify_pending: false
```
`notify_pending` 默认为关闭状态,当该项设置为 `false` 时,待审评论不会发送通知。你可以在控制中心查看所有待审核的评论。
## 嘈杂模式 `noise_mode`
```yaml
@@ -236,7 +245,7 @@ admin_notify:
noise_mode: false
```
noise_mode 默认为关闭状态,当该项设置为 `false` 时,站内仅向管理员回复的消息会发送通知,例如「普通用户 A」回复「普通用户 B」,这两个用户之间的通讯不会通知管理员。
`noise_mode` 默认为关闭状态,当该项设置为 `false` 时,站内仅向管理员回复的消息会发送通知,例如「普通用户 A」回复「普通用户 B」,这两个用户之间的通讯不会通知管理员。
注:当 `moderator.pending_default` 为 `true` 时,noise_mode 为始终开启状态。
+1
View File
@@ -96,6 +96,7 @@ db:
charset: "utf8mb4" # 编码格式
table_prefix: "" # 表前缀 (例如:"atk_")
ssl: false # 启用 SSL
prepare_stmt: true # 预编译语句
```
数据表将在 Artalk 启动时自动完成创建,无需额外操作。
+3 -3
View File
@@ -1,10 +1,10 @@
# 在后端控制前端
你可以在后端完全控制前端界面的行为(包括设定[前端的配置](/guide/frontend/config)),我们推荐使用这种方式部署 Artalk,并且这个特性「默认开启」。
你可以在后端完全控制前端界面的行为(包括设定几乎所有[前端配置项](/guide/frontend/config)),我们推荐使用这种方式配置 Artalk 界面,并且这个特性「默认开启」。
## 如何修改前端配置
## 如何修改前端界面配置
在侧边栏的[控制中心](/guide/frontend/sidebar.md#控制中心)提供了图形设置界面,你能快速轻松地对前端配置进行修改。
在侧边栏的[控制中心](/guide/frontend/sidebar.md#控制中心)提供了图形设置界面,你能快速轻松地对前端界面配置进行修改。
<img src="../../images/sidebar/fe-setting.png" width="400px">
+12 -90
View File
@@ -1,16 +1,10 @@
# 管理员 × 多站点
Artalk 支持站点隔离,以及为站点分配指定的管理员账户,
Artalk 支持多站点,管理员可以创建多个站点,并对不同站点的评论、页面进行集中管理。
所以,你可以实现两种模式:单人多站点、多人多站点。
## 创建管理员
运用「单人多站点」模式,你能够实现多个站点共用同一个 Artalk 后端程序,集中化管理。
运用「多人多站点」模式,你可以和你的朋友们共用同一个 Artalk 后端程序。😉
## 创建超级管理员
执行命令根据命令行提示快速创建超级管理员。
执行命令根据命令行提示快速创建管理员。
```sh
./artalk admin
@@ -22,7 +16,7 @@ Artalk 支持站点隔离,以及为站点分配指定的管理员账户,
docker exec -it artalk artalk admin
```
之后可使用超级管理员账户登陆控制中心,在用户管理页面的图形界面可创建更多账户,而无需手动编辑配置文件。
之后可使用管理员账户登录控制中心,在用户管理页面的图形界面可创建更多账户,无需手动编辑配置文件。
## 站点的创建和管理
@@ -32,25 +26,25 @@ docker exec -it artalk artalk admin
你可以设置多个管理员账户,当输入框输入匹配管理员用户名和邮箱时,将弹出密码验证提示框,
并且只有管理员才能访问侧边栏「[控制中心](../frontend/sidebar.md#控制中心)」,在前端对评论内容进行管理操作。
并且只有管理员才能访问「[控制中心](../frontend/sidebar.md#控制中心)」,在前端对评论内容进行管理操作。
在配置文件中添加如下内容:
(可选操作)通过配置文件添加管理员:
```yaml
admin_users:
- name: "admin"
email: "admin@example.com"
password: ""
password: "(bcrypt)$2y$10$ti4vZYIrxVN8rLcYXVgXCO.GJND0dyI49r7IoF3xqIx8bBRmIBZRm"
badge_name: "管理员"
badge_color: "#0083FF"
- name: "admin2"
email: "admin2@example.com"
password: ""
password: "(bcrypt)$2y$10$ti4vZYIrxVN8rLcYXVgXCO.GJND0dyI49r7IoF3xqIx8bBRmIBZRm"
badge_name: "小管理员"
badge_color: "#0083FF"
```
每一项配置的解释:
每项配置的解释:
- **name** & **email**:用户名和邮箱,**不区分大小写**。
- **password**:用户密码。
@@ -71,74 +65,9 @@ admin_users:
- **badge_name**:用户显示的头衔徽标文字。
- **badge_color**:用户显示的头衔徽标背景颜色。
## 超级管理员、普通管理员
管理员分为「超级管理员」和「普通管理员」,前者可以管理全部站点,后者只能管理部分站点。
普通管理员将:
- 不会收到其它站点的评论邮件通知,邮件通知彼此隔离。
- 不能对设定范围外的站点进行 删除、修改、查询 等操作。
- 仅能在受限模式下使用 数据导入导出、站点管理 等功能。
完整的 `admin_users` 配置如下:
```yaml
admin_users:
- name: "超级管理员"
email: "super_admin@example.com"
password: "(bcrypt)$2y$10$ti4vZYIrxVN8rLcYXVgXCO.GJND0dyI49r7IoF3xqIx8bBRmIBZRm"
badge_name: "超级管理员"
badge_color: "#0083ff"
receive_email: false
- name: "普通管理员1"
email: "admin1@example.com"
password: "(bcrypt)$2y$10$ti4vZYIrxVN8rLcYXVgXCO.GJND0dyI49r7IoF3xqIx8bBRmIBZRm"
badge_name: "管理员"
badge_color: "#0083ff"
sites:
- 站点名 A
- 站点名 B
- name: "普通管理员2"
email: "admin2@example.com"
password: "(bcrypt)$2y$10$ti4vZYIrxVN8rLcYXVgXCO.GJND0dyI49r7IoF3xqIx8bBRmIBZRm"
badge_name: "管理员"
badge_color: "#0083ff"
sites:
- 站点名 C
- 站点名 D
```
### 为管理员分配站点
- **sites**:向管理员分配站点,即控制管理员对于站点「[控制中心](../frontend/sidebar.md#控制中心)」的可访问权限。
当未分配站点时,即 sites 未配置时,该账户为「**超级管理员**」,可以管理全部站点:
```yaml
admin_users:
- name: "super_admin"
# 未分配 sites,为超级管理员
```
当分配站点后,即 sites 已配置时,该管理员成为「**普通管理员**」:
```yaml
admin_users:
- name: "a_admin_user"
# ↓ 为账户分配 sites
sites:
- 站点名 A
- 站点名 B
```
注:你至少需要一个超级管理员,这样才能创建站点。
### 控制管理员接收邮件通知
当页面有新的评论时,邮件会发送给**该站点**的全部管理员,但你可以配置 `receive_email` 强制禁用它。
当页面有新的评论时,默认配置是邮件会发送给全部管理员,但你可以配置 `receive_email` 强制禁用它。
这对于设定了多个邮箱,但又不希望某些邮箱收到评论邮件的情况很有帮助。
@@ -148,13 +77,6 @@ admin_users:
```yaml
admin_users:
- name: "super_admin"
# 未分配 sites,即为超级管理员
receive_email: false # ← 超级管理员不接收邮件
- name: "a_admin_user"
# 分配 sites,即为普通管理员
sites:
- 站点名 A
- 站点名 B
- name: "admin"
receive_email: false # ← 强制不接收邮件
```
+12 -6
View File
@@ -15,7 +15,7 @@ docker run -d \
> 假设域名 `http://your_domain` 已正确添加 DNS 记录并指向你的服务器 IP
浏览器打开 `http://your_domain:8080` 将出现 Artalk 后台登陆界面。
浏览器打开 `http://your_domain:8080` 将出现 Artalk 后台登录界面。
执行命令创建管理员账户:
@@ -127,21 +127,27 @@ Artalk 静态资源通过上游 CDNJS 分发,以下镜像资源可供选择:
>
> https://cdnjs.cloudflare.com/ajax/libs/artalk/:ArtalkVersion:/Artalk.css
**Staticfile CDN (国内)**
**SUSTech Mirrors (境内)**
> https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/artalk/:ArtalkVersion:/Artalk.js
>
> https://mirrors.sustech.edu.cn/cdnjs/ajax/libs/artalk/:ArtalkVersion:/Artalk.css
::: details 查看更多
**Staticfile CDN (境内)**
> https://cdn.staticfile.org/artalk/:ArtalkVersion:/Artalk.js
>
> https://cdn.staticfile.org/artalk/:ArtalkVersion:/Artalk.css
::: details 查看更多
**BootCDN (国内)**
**BootCDN (境内)**
> https://cdn.bootcdn.net/ajax/libs/artalk/:ArtalkVersion:/Artalk.js
>
> https://cdn.bootcdn.net/ajax/libs/artalk/:ArtalkVersion:/Artalk.css
**75CDN (国内)**
**75CDN (境内)**
> https://lib.baomitu.com/artalk/:ArtalkVersion:/Artalk.js
>
+1 -1
View File
@@ -7,7 +7,7 @@ artalk.update({ ... })
```
- 默认配置:[defaults.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/src/defaults.ts)
- 声明文件:[config.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/types/config.ts)
- 声明文件:[config.ts](https://github.com/ArtalkJS/Artalk/blob/master/ui/artalk/src/types/config.ts)
## 轻松配置
+1 -1
View File
@@ -57,7 +57,7 @@ Artalk 除了支持 OwO 格式外,还内置支持一种标准的表情包列
::: warning
前端的配置将默认被后端覆盖,详情参考:[在后端控制前端](/guide/backend/fe-control.html)
前端的配置可能会被后端所覆盖,详情参考:[在后端控制前端](/guide/backend/fe-control.html)
:::
+8 -8
View File
@@ -1,14 +1,14 @@
# IP 归属地
# IP 属地
Artalk 内置用户 IP 归属地展示功能,并且你可以设置显示的精度:精确到市、省。
Artalk 内置用户 IP 属地展示功能,并且你可以设置显示的精度:精确到市、省。
该功能默认关闭,你可在 Artalk 控制中心设置中启用 IP 归属地展示功能。
该功能默认关闭,你可在 Artalk 控制中心设置中启用 IP 属地展示功能。
## IP 归属地数据库
## IP 属地数据库
在开启 IP 归属地展示功能之前,你需要下载一个数据库文件:
在开启 IP 属地展示功能之前,你需要下载一个数据库文件:
- [GitHub 下载](https://github.com/lionsoul2014/ip2region/raw/master/data/ip2region.xdb) / [镜像站下载](https://ghproxy.com/?q=https%3A%2F%2Fgithub.com%2Flionsoul2014%2Fip2region%2Fblob%2Fmaster%2Fdata%2Fip2region.xdb) (国内推荐)
- [GitHub 下载](https://github.com/lionsoul2014/ip2region/raw/master/data/ip2region.xdb) / [镜像站下载](https://gh-proxy.com/?q=https%3A%2F%2Fgithub.com%2Flionsoul2014%2Fip2region%2Fblob%2Fmaster%2Fdata%2Fip2region.xdb) (境内推荐)
下载后请手动放置到 `./data/` 目录下,文件命名为:`ip2region.xdb`
@@ -25,9 +25,9 @@ Artalk 内置用户 IP 归属地展示功能,并且你可以设置显示的精
配置文件:
```yaml
# IP 归属地
# IP 属地
ip_region:
# 启用 IP 归属地展示
# 启用 IP 属地展示
enabled: false
# 数据文件路径 (.xdb 格式)
db_path: "./data/ip2region.xdb"
+1 -1
View File
@@ -40,4 +40,4 @@
## 设置
登陆管理员账户可进入到 “控制中心” 的图形设置界面,轻松地对 [配置文件](/guide/backend/config.md) 以及 [前端的配置](/guide/frontend/config.md) 进行修改,而无需手动编辑复杂的配置文件。
登录管理员账户可进入到 “控制中心” 的图形设置界面,轻松地对 [配置文件](/guide/backend/config.md) 以及 [前端的配置](/guide/frontend/config.md) 进行修改,而无需手动编辑复杂的配置文件。
+17 -4
View File
@@ -1,6 +1,19 @@
# 安全防范
## 用户身份凭证存储
## 程序运行安全性
有以下一些安全措施可以提高服务器的安全性:
1. 确保 Artalk 运行在非 `root` 用户或 Docker 容器中,让 Artalk 运行在较低的权限以及隔离的环境中,以降低程序被攻击后的危害。
2. 保持 Artalk 的版本为最新,新版可能修复了一些安全漏洞,以及提升了安全性。
3. 确保 Artalk 运行在 HTTPS 协议下,以防止数据被中间人窃取。
4. 设置强度足够的密码,并配置好验证码等安全措施,以防止密码被破解。
5. 设置 `trusted_domains`,以防止恶意的跨域请求。
6. 定期备份数据库,以防止数据丢失。
## 程序设计安全性
### 用户身份凭证存储
Artalk 在努力保护用户的安全。参考 [OWASP 安全备忘录](https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html) 中提到的内容,Artalk 的开发对于来自用户输入的任何数据始终持以怀疑的态度,我们严格遵循以下准则防范 XSS 攻击:
@@ -10,16 +23,16 @@ Artalk 在努力保护用户的安全。参考 [OWASP 安全备忘录](https://c
- 不要去调用一些高风险的内置函数,因为会带来潜在的安全隐患。例如:`eval()`。
- 注意来自内置 API 的数据,应任被视为不可信的。例如:`location.hash.split("#")[1]`。
## 跨域请求安全性
### 跨域请求安全性
参考:[Cross-Origin Resource Sharing (CORS) | MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
【待补充】
## API 设计安全性
### API 设计安全性
【待补充】
## 写在结尾
Artalk 的功能在不断完善,同时也在不断向提高安全性的目标努力,但在努力的同时还是难免发生一些疏忽和遗漏,若遇问题或有好的建议,欢迎反馈与指正。
Artalk 的功能在不断完善,同时也在不断向提高安全性的目标努力,但难免存在可能的问题,如果您发现了 Artalk 的安全漏洞,请通过 [GitHub Issues](https://github.com/ArtalkJS/Artalk/issues) 或者邮箱 artalkjs@gmail.com 快速向我们取得联系,我们会尽最大的努力在第一时间解决!感谢您的支持!
+7 -5
View File
@@ -165,15 +165,15 @@ typecho-20220424-202246.artrans
参数格式遵循 `<key>:<value>`,例如:
```bash
./artalk import 类型 t_name:"Site" t_url:"https://xx.com" json_file:"文件路径"
./artalk import 类型 target_site_name:"Site" target_site_url:"https://xx.com" json_file:"文件路径"
```
前端的导入同样可以手动输入启动参数,例如:
```json
{
"t_name": "Site",
"t_url": "https://xx.com",
"target_site_name": "Site",
"target_site_url": "https://xx.com",
"json_file": "服务器上的文件路径"
}
```
@@ -182,10 +182,12 @@ Artalk 导入功能的通用启动参数:
| 参数 | 类型 | 说明 |
| :------: | ------ | ------------ |
| `t_name` | String | 导入站点名称 |
| `t_url` | String | 导入站点 URL |
| `target_site_name` | String | 导入站点名称 |
| `target_site_url` | String | 导入站点 URL |
| `url_resolver` | Boolean | 默认关闭,URL 解析器。将 `page_key` 基于 `target_site_url` 参数重新生成为完整 URL 作为评论的新 `page_key` |
| `json_file` | String | JSON 数据文件路径 |
| `json_data` | String | JSON 数据字符串内容 |
| `assumeyes` | Boolean | 不提确认 `y/n`,直接执行 |
## 数据备份
+3 -3
View File
@@ -17,10 +17,10 @@
"devDependencies": {
"artalk": "workspace:^",
"markdown-it-for-inline": "^2.0.1",
"sass": "^1.69.5",
"vitepress": "1.0.0-rc.32"
"sass": "^1.70.0",
"vitepress": "1.0.0-rc.40"
},
"dependencies": {
"vue": "^3.3.12"
"vue": "^3.4.15"
}
}
+6 -6
View File
@@ -19,19 +19,19 @@
"normalize.css": "^8.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.12.0"
"react-icons": "^5.0.1"
},
"devDependencies": {
"@types/react": "^18.2.45",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.56.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"sass": "^1.69.5",
"sass": "^1.70.0",
"typescript": "^5.3.3",
"vite": "^5.0.10"
"vite": "^5.0.12"
}
}
+4057 -2767
View File
File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -11,9 +11,10 @@
"private": true,
"scripts": {
"swagger:build": "npx @redocly/cli build-docs --output ./dist/http-api.html",
"swagger:serve": "npx @redocly/cli preview-docs"
"swagger:serve": "npx @redocly/cli preview-docs",
"swagger:build-http-client": "npx swagger-typescript-api -n v2 -p ./swagger.json -o ../../ui/artalk/src/api --responses --sort-types"
},
"devDependencies": {
"@redocly/cli": "1.5.0"
"@redocly/cli": "1.8.1"
}
}
+4682 -3276
View File
File diff suppressed because it is too large. Load diff
+2574 -1771
View File
File diff suppressed because it is too large. Load diff
+69 -53
View File
@@ -8,68 +8,73 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/blang/semver v3.5.1+incompatible
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874
github.com/cheggaaa/pb/v3 v3.1.4
github.com/cheggaaa/pb/v3 v3.1.5
github.com/eko/gocache/lib/v4 v4.1.5
github.com/eko/gocache/store/bigcache/v4 v4.2.1
github.com/eko/gocache/store/memcache/v4 v4.2.1
github.com/eko/gocache/store/redis/v4 v4.2.1
github.com/fatih/color v1.15.0
github.com/fatih/color v1.16.0
github.com/go-testfixtures/testfixtures/v3 v3.9.0
github.com/gofiber/fiber/v2 v2.49.2
github.com/gofiber/swagger v0.1.13
github.com/gofiber/fiber/v2 v2.52.0
github.com/gofiber/swagger v0.1.14
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/jedib0t/go-pretty/v6 v6.4.8
github.com/jedib0t/go-pretty/v6 v6.5.4
github.com/jeremywohl/flatten v1.0.1
github.com/knadh/koanf v1.5.0
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20231008093706-3ef87ff7272b
github.com/microcosm-cc/bluemonday v1.0.25
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20231013030745-3066d243cd04
github.com/microcosm-cc/bluemonday v1.0.26
github.com/nikoksr/notify v0.41.0
github.com/qwqcode/go-aliyun-email v0.0.0-20180120030821-cb6e7b1382bf
github.com/redis/go-redis/v9 v9.2.1
github.com/redis/go-redis/v9 v9.4.0
github.com/rhysd/go-github-selfupdate v1.2.3
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/samber/lo v1.38.1
github.com/samber/lo v1.39.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/cobra v1.8.0
github.com/steambap/captcha v1.4.1
github.com/stretchr/testify v1.8.4
github.com/swaggo/swag v1.16.2
github.com/tidwall/gjson v1.17.0
github.com/vmihailenco/msgpack v4.0.4+incompatible
github.com/x-cray/logrus-prefixed-formatter v0.5.2
github.com/yuin/goldmark v1.5.6
golang.org/x/crypto v0.14.0
golang.org/x/exp v0.0.0-20231006140011-7918f672742d
golang.org/x/sync v0.4.0
golang.org/x/term v0.13.0
github.com/yuin/goldmark v1.6.0
golang.org/x/crypto v0.18.0
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
golang.org/x/sync v0.6.0
golang.org/x/term v0.16.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.2
gorm.io/driver/postgres v1.5.3
gorm.io/driver/postgres v1.5.4
gorm.io/driver/sqlite v1.5.4
gorm.io/driver/sqlserver v1.5.2
gorm.io/gorm v1.25.5
gorm.io/gorm v1.25.6
)
require (
github.com/ClickHouse/ch-go v0.58.2 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.14.2 // indirect
github.com/ClickHouse/ch-go v0.61.2 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.17.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blinkbean/dingtalk v0.0.0-20230927120905-796332ac4ba1 // indirect
github.com/blinkbean/dingtalk v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/dmarkham/enumer v1.5.9 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.6.1 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/spec v0.20.14 // indirect
github.com/go-openapi/swag v0.22.9 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
@@ -77,43 +82,48 @@ require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.2 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/klauspost/compress v1.17.5 // indirect
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mattn/go-sqlite3 v1.14.21 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microsoft/go-mssqldb v1.6.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.28.0 // indirect
github.com/paulmach/orb v0.10.0 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/onsi/gomega v1.31.1 // indirect
github.com/pascaldekloe/name v1.0.1 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/common v0.46.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/slack-go/slack v0.12.3 // indirect
@@ -127,19 +137,25 @@ require (
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/utahta/go-linenotify v0.5.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.50.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/tools v0.14.0 // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.26.0 // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.17.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)
+144 -120
View File
@@ -19,12 +19,15 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDm
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
github.com/ClickHouse/clickhouse-go/v2 v2.14.2 h1:iYGP2bgPYJ33y6rCfZTQAPrnHt8wmsM3Ut4cLoYhWY0=
github.com/ClickHouse/clickhouse-go/v2 v2.14.2/go.mod h1:ZLn63wODwGxVdnGB0EIYmFL5tjtlLcLBuwQUH6B2sYk=
github.com/ClickHouse/ch-go v0.61.2 h1:8+8eKO2VgxoRa0yLJpWwkqJxi/jrtP5Z+J6eZdPfwdc=
github.com/ClickHouse/ch-go v0.61.2/go.mod h1:ZSVIE1A7mGJNcJeBvVF1v5bo12n0Wmnw30RhnPCpLzg=
github.com/ClickHouse/clickhouse-go v1.5.4 h1:cKjXeYLNWVJIx2J1K6H2CqyRmfwVJVY1OV1coaaFcI0=
github.com/ClickHouse/clickhouse-go/v2 v2.17.1 h1:ZCmAYWpu75IyEi7+Yrs/uaAjiCGY5wfW5kXo64exkX4=
github.com/ClickHouse/clickhouse-go/v2 v2.17.1/go.mod h1:rkGTvFDTLqLIm0ma+13xmcCfr/08Gvs7KmFt1tgiWHQ=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
@@ -36,8 +39,12 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
@@ -66,8 +73,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blinkbean/dingtalk v0.0.0-20230927120905-796332ac4ba1 h1:G14RkaB3RRW099aQbfyHm4RFgNxGUOTeHSoN+CZy2YI=
github.com/blinkbean/dingtalk v0.0.0-20230927120905-796332ac4ba1/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
github.com/blinkbean/dingtalk v1.1.1 h1:3hvJJUk/YE9uAVOP7q8HyIcLxfGCQqkAKEa63giB2Ns=
github.com/blinkbean/dingtalk v1.1.1/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
@@ -78,15 +85,15 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo=
github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA=
github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk=
github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -95,6 +102,8 @@ github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+
github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dmarkham/enumer v1.5.9 h1:NM/1ma/AUNieHZg74w67GkHFBNB15muOt3sj486QVZk=
github.com/dmarkham/enumer v1.5.9/go.mod h1:e4VILe2b1nYK3JKJpRmNdl5xbDQvELc6tQ8b+GsGk6E=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -113,18 +122,18 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -132,23 +141,25 @@ github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
@@ -162,11 +173,11 @@ github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a
github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY=
github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.49.1/go.mod h1:nPUeEBUeeYGgwbDm59Gp7vS8MDyScL6ezr/Np9A13WU=
github.com/gofiber/fiber/v2 v2.49.2 h1:ONEN3/Vc+dUCxxDgZZwpqvhISgHqb+bu+isBiEyKEQs=
github.com/gofiber/fiber/v2 v2.49.2/go.mod h1:gNsKnyrmfEWFpJxQAV0qvW6l70K1dZGno12oLtukcts=
github.com/gofiber/swagger v0.1.13 h1:e7tMH/y34HIMklQ2+uv64Ai3E8TWCgbZyscIx+IzInE=
github.com/gofiber/swagger v0.1.13/go.mod h1:VtNHZdI5ksFlIR1R0vCcCX3/ruT8p9xNRX44958rsao=
github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw=
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/swagger v0.1.14 h1:o524wh4QaS4eKhUCpj7M0Qhn8hvtzcyxDsfZLXuQcRI=
github.com/gofiber/swagger v0.1.14/go.mod h1:DCk1fUPsj+P07CKaZttBbV1WzTZSQcSxfub8y9/BFr8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
@@ -213,8 +224,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
@@ -224,15 +235,16 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/uuid v0.0.0-20171113160352-8c31c18f31ed/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ=
@@ -259,6 +271,8 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -288,22 +302,24 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/pgx/v5 v5.5.2 h1:iLlpgp4Cp/gC9Xuscl7lFL1PhhW+ZLtXZcrfCt4C3tA=
github.com/jackc/pgx/v5 v5.5.2/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jedib0t/go-pretty/v6 v6.4.8 h1:HiNzyMSEpsBaduKhmK+CwcpulEeBrTmxutz4oX/oWkg=
github.com/jedib0t/go-pretty/v6 v6.4.8/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
github.com/jedib0t/go-pretty/v6 v6.5.4 h1:gOGo0613MoqUcf0xCj+h/V3sHDaZasfv152G6/5l91s=
github.com/jedib0t/go-pretty/v6 v6.5.4/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg=
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -330,8 +346,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.5 h1:d4vBd+7CHydUqpFBgUEKkSdtSugf9YFmSkvUYPquI5E=
github.com/klauspost/compress v1.17.5/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -339,7 +355,6 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -352,8 +367,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20231008093706-3ef87ff7272b h1:5htBifSGhpXFOGTCfTFz0x5Kn41UyHRN6ovFGV376G8=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20231008093706-3ef87ff7272b/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20231013030745-3066d243cd04 h1:FfdA6VoXuyjsRqZlbm5H97zGtmlcM/yB6k1Expsym1E=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20231013030745-3066d243cd04/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@@ -370,21 +385,19 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.21 h1:IXocQLOykluc3xPE0Lvy8FtggMz1G+U3mEjg+0zGizc=
github.com/mattn/go-sqlite3 v1.14.21/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc=
github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
@@ -394,6 +407,7 @@ github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXx
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -429,19 +443,21 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.28.0 h1:i2rg/p9n/UqIDAMFUJ6qIUUMcsqOuUHgbpbu235Vr1c=
github.com/onsi/gomega v1.28.0/go.mod h1:A1H2JE76sI14WIP57LMKj7FVfCHx3g3BcZVjJG8bjX8=
github.com/onsi/gomega v1.31.1 h1:KYppCUK+bUgAZwHOu7EXVBKyQA6ILvOESHkn/tgoqvo=
github.com/onsi/gomega v1.31.1/go.mod h1:y40C95dwAD1Nz36SsEnxvfFe8FFfNxzI5eJ0EYGyAy0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s=
github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0=
github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM=
github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -449,7 +465,6 @@ github.com/pkg/errors v0.8.1-0.20161029093637-248dadf4e906/go.mod h1:bwawxfHBFNV
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@@ -458,8 +473,8 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -469,8 +484,8 @@ github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
@@ -479,8 +494,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/qwqcode/go-aliyun-email v0.0.0-20180120030821-cb6e7b1382bf h1:FH9O7I17HCdVYbK1IMxTGgD3bVuTzyO45bR94iE+T/U=
github.com/qwqcode/go-aliyun-email v0.0.0-20180120030821-cb6e7b1382bf/go.mod h1:5N3D7E1/M3G95YK4jPjcPRf/A7XBis0ni4VMmcVnnRY=
github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg=
github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
@@ -488,18 +503,18 @@ github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnH
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg=
github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
@@ -514,8 +529,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.12.3 h1:92/dfFU8Q5XP6Wp5rr5/T5JHLM5c5Smtn53fhToAP88=
github.com/slack-go/slack v0.12.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/steambap/captcha v1.4.1 h1:OmMdxLCWCqJvsFaFYwRpvMckIuvI6s8s1LsBrBw97P0=
@@ -533,7 +548,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
@@ -564,15 +578,15 @@ github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M=
github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v5 v5.4.0 h1:hRM0digJwyR6vll33NNAwCFguy5JuBD6jxDmQP3l608=
github.com/vmihailenco/msgpack/v5 v5.4.0/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
@@ -585,19 +599,27 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA=
github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg=
go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo=
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
@@ -610,14 +632,14 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -630,8 +652,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -655,22 +677,24 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sync v0.0.0-20171101214715-fd80eb99c8f6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -683,8 +707,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -727,25 +751,26 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -759,8 +784,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -779,8 +804,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -816,8 +841,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
@@ -841,7 +866,6 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -851,16 +875,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/driver/postgres v1.5.3 h1:qKGY5CPHOuj47K/VxbCXJfFvIUeqMSXXadqdCY+MbBU=
gorm.io/driver/postgres v1.5.3/go.mod h1:F+LtvlFhZT7UBiA81mC9W6Su3D4WUhSboc/36QZU0gk=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/driver/sqlserver v1.5.2 h1:+o4RQ8w1ohPbADhFqDxeeZnSWjwOcBnxBckjTbcP4wk=
gorm.io/driver/sqlserver v1.5.2/go.mod h1:gaKF0MO0cfTq9Q3/XhkowSw4g6nIwHPGAs4hzKCmvBo=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.2-0.20230610234218-206613868439/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
+1
View File
@@ -40,6 +40,7 @@
"Password update failed":
"Password updated":
"Password":
"Pending":
"Please review":
"Retype {{name}}":
"Save failed":
+2 -1
View File
@@ -27,7 +27,7 @@
"Invalid request. Please check your `trusted_domains` config.": 请求无效, 请检查 `trusted_domains` 配置项
"Invalid {{name}}": 无效的{{name}}
"Link": 链接
"Login failed": 登陆失败
"Login failed": 登录失败
"Name": 名称
"New version available": 有更新可用
"Nickname": 昵称
@@ -40,6 +40,7 @@
"Password update failed": 密码修改失败
"Password updated": 密码已修改
"Password": 密码
"Pending": 待审核
"Please review": 请过目
"Retype {{name}}": 重新输入{{name}}
"Save failed": 保存失败
+2 -1
View File
@@ -27,7 +27,7 @@
"Invalid request. Please check your `trusted_domains` config.": 請求無效, 請檢查 `trusted_domains` 配置項
"Invalid {{name}}": 無效的{{name}}
"Link": 鏈接
"Login failed": 登陸失敗
"Login failed": 登錄失敗
"Name": 名稱
"New version available": 有更新可用
"Nickname": 暱稱
@@ -40,6 +40,7 @@
"Password update failed": 密碼修改失敗
"Password updated": 密碼已修改
"Password": 密碼
"Pending": 待審核
"Please review": 請過目
"Retype {{name}}": 重新輸入{{name}}
"Save failed": 保存失敗
+6 -10
View File
@@ -11,10 +11,6 @@ func RunExportArtrans(dao *dao.Dao, params *ExportParams) (string, error) {
}
func RunImportArtrans(dao *dao.Dao, params *ImportParams) {
if !params.UrlResolver {
logWarn("Target site URL resolver disabled")
}
// 读取 JSON
if params.JsonData == "" {
if params.JsonFile == "" {
@@ -50,12 +46,12 @@ func ArrToImportParams(arr []string) *ImportParams {
params.UrlResolver = false // 默认关闭
getParamsFrom(arr).To(map[string]any{
"t_name": &params.TargetSiteName,
"t_url": &params.TargetSiteUrl,
"t_url_resolver": &params.UrlResolver,
"json_file": &params.JsonFile,
"json_data": &params.JsonData,
"assumeyes": &params.Assumeyes,
"target_site_name": &params.TargetSiteName,
"target_site_url": &params.TargetSiteUrl,
"url_resolver": &params.UrlResolver,
"json_file": &params.JsonFile,
"json_data": &params.JsonData,
"assumeyes": &params.Assumeyes,
})
return &params
+12 -8
View File
@@ -15,12 +15,12 @@ import (
)
type ImportParams struct {
TargetSiteName string
TargetSiteUrl string
UrlResolver bool
JsonFile string
JsonData string
Assumeyes bool // Automatically answer yes for all questions.
TargetSiteName string `json:"target_site_name" form:"target_site_name" validate:"optional"` // The target site name
TargetSiteUrl string `json:"target_site_url" form:"target_site_url" validate:"optional"` // The target site url
UrlResolver bool `json:"url_resolver" form:"url_resolver" validate:"optional"` // Enable URL resolver
JsonFile string `json:"json_file,omitempty" form:"json_file" validate:"optional"` // The JSON file path
JsonData string `json:"json_data,omitempty" form:"json_data" validate:"optional"` // The JSON data
Assumeyes bool `json:"assumeyes" form:"assumeyes" validate:"optional"` // Automatically answer yes for all questions.
}
func importArtrans(dao *dao.Dao, params *ImportParams, comments []*entity.Artran) {
@@ -106,8 +106,8 @@ func importArtrans(dao *dao.Dao, params *ImportParams, comments []*entity.Artran
}
// 准备 user
user := dao.FindCreateUser(c.Nick, c.Email, c.Link)
if !user.IsAdmin {
user, err := dao.FindCreateUser(c.Nick, c.Email, c.Link)
if err == nil && !user.IsAdmin {
userModified := false
if c.BadgeName != "" && c.BadgeName != user.BadgeName {
user.BadgeName = c.BadgeName
@@ -126,6 +126,10 @@ func importArtrans(dao *dao.Dao, params *ImportParams, comments []*entity.Artran
nPageKey := c.PageKey
if params.UrlResolver { // 使用 URL 解析器
splittedURLs := utils.SplitAndTrimSpace(params.TargetSiteUrl, ",")
if len(splittedURLs) == 0 {
logFatal("[URL Resolver] " + i18n.T("{{name}} cannot be empty", map[string]interface{}{"name": i18n.T("Target Site") + " " + "URL"}))
return
}
nPageKey = urlResolverGetPageKey(splittedURLs[0], c.PageKey)
}
+11 -12
View File
@@ -29,21 +29,20 @@
gt.appendTo("#captcha").onSuccess(function (e) {
var result = gt.getValidate();
var form = new FormData()
form.append("value", JSON.stringify(result))
console.log("[极验验证结果] ", result)
fetch("./check", {
fetch("./verify", {
method: 'POST',
body: form,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ value: JSON.stringify(result) }),
})
.then(function (resp) {
return resp.json()
})
.then(function (json) {
if (json.success) {
console.log("验证成功:" + json)
} else {
alert("验证失败:" + json.msg || '')
.then(function (res) {
if (!res.ok) {
res.json().then(json => {
alert("验证失败:" + res.status + " " + json.msg || '')
})
}
})
.catch(function (err) {
+11 -12
View File
@@ -20,20 +20,19 @@
hcaptcha.render("container", {
sitekey: "{{.site_key}}",
callback: function(token) {
var form = new FormData()
form.append("value", token)
fetch("./check", {
fetch("./verify", {
method: 'POST',
body: form,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ value: token }),
})
.then(function (resp) {
return resp.json()
})
.then(function (json) {
if (json.success) {
console.log("验证成功:" + json)
} else {
alert("验证失败:" + json.msg || '')
.then(function (res) {
if (!res.ok) {
res.json().then(json => {
alert("验证失败:" + res.status + " " + json.msg || '')
})
}
})
.catch(function (err) {
+11 -12
View File
@@ -18,20 +18,19 @@
grecaptcha.render("container", {
sitekey: "{{.site_key}}",
callback: function(token) {
var form = new FormData()
form.append("value", token)
fetch("./check", {
fetch("./verify", {
method: 'POST',
body: form,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ value: token }),
})
.then(function (resp) {
return resp.json()
})
.then(function (json) {
if (json.success) {
console.log("验证成功:" + json)
} else {
alert("验证失败:" + json.msg || '')
.then(function (res) {
if (!res.ok) {
res.json().then(json => {
alert("验证失败:" + res.status + " " + json.msg || '')
})
}
})
.catch(function (err) {
+11 -12
View File
@@ -18,20 +18,19 @@
const turnstileOptions = {
sitekey: '{{.site_key}}',
callback: function(token) {
var form = new FormData()
form.append("value", token)
fetch("./check", {
fetch("./verify", {
method: 'POST',
body: form,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ value: token }),
})
.then(function (resp) {
return resp.json()
})
.then(function (json) {
if (json.success) {
console.log("验证成功:" + json)
} else {
alert("验证失败:" + json.msg || '')
.then(function (res) {
if (!res.ok) {
res.json().then(json => {
alert("验证失败:" + res.status + " " + json.msg || '')
})
}
})
.catch(function (err) {
+5 -1
View File
@@ -169,6 +169,10 @@ func (conf *Config) i18nPatch() {
// IP属地功能配置修补
func (conf *Config) ipRegionPatch() {
if !conf.IPRegion.Enabled {
return
}
// IP 属地默认数据文件
if conf.IPRegion.DBPath == "" {
conf.IPRegion.DBPath = "./data/ip2region.xdb"
@@ -176,7 +180,7 @@ func (conf *Config) ipRegionPatch() {
// 检测配置文件是否存在
if !utils.CheckFileExist(conf.IPRegion.DBPath) {
log.Warn("未找到 IP 数据库文件:" + strconv.Quote(conf.IPRegion.DBPath) + ",IP 归属地功能已禁用," +
log.Warn("未找到 IP 数据库文件:" + strconv.Quote(conf.IPRegion.DBPath) + ",IP 属地功能已禁用," +
"参考链接:https://artalk.js.org/guide/frontend/ip-region.html")
conf.IPRegion.Enabled = false
}
+12 -16
View File
@@ -20,12 +20,11 @@ type Config struct {
SSL SSLConf `koanf:"ssl" json:"ssl"` // SSL
SiteDefault string `koanf:"site_default" json:"site_default"` // 默认站点名(当请求无指定 site_name 时使用)
AdminUsers []AdminUserConf `koanf:"admin_users" json:"admin_users"` // 管理员账户
LoginTimeout int `koanf:"login_timeout" json:"login_timeout"` // 登陆超时
Cookie CookieConf `koanf:"cookie" json:"cookie"` // Cookie
LoginTimeout int `koanf:"login_timeout" json:"login_timeout"` // 登录超时
Moderator ModeratorConf `koanf:"moderator" json:"moderator"` // 评论审查
Captcha CaptchaConf `koanf:"captcha" json:"captcha"` // 验证码
Email EmailConf `koanf:"email" json:"email"` // 邮箱提醒
IPRegion IPRegionConf `koanf:"ip_region" json:"ip_region"` // IP 归属地展示
IPRegion IPRegionConf `koanf:"ip_region" json:"ip_region"` // IP 属地展示
ImgUpload ImgUploadConf `koanf:"img_upload" json:"img_upload"` // 图片上传
AdminNotify AdminNotifyConf `koanf:"admin_notify" json:"admin_notify"` // 其他通知方式
Frontend map[string]interface{} `koanf:"frontend" json:"frontend"`
@@ -58,6 +57,7 @@ type DBConf struct {
TablePrefix string `koanf:"table_prefix" json:"table_prefix"`
Charset string `koanf:"charset" json:"charset"`
SSL bool `koanf:"ssl" json:"ssl"`
PrepareStmt *bool `koanf:"prepare_stmt" json:"prepare_stmt"`
}
type CacheConf struct {
@@ -93,18 +93,13 @@ type SSLConf struct {
}
type AdminUserConf struct {
Name string `koanf:"name" json:"name"`
Email string `koanf:"email" json:"email"`
Link string `koanf:"link" json:"link"`
Password string `koanf:"password" json:"password"`
BadgeName string `koanf:"badge_name" json:"badge_name"`
BadgeColor string `koanf:"badge_color" json:"badge_color"`
ReceiveEmail *bool `koanf:"receive_email" json:"receive_email"`
Sites []string `koanf:"sites" json:"sites"`
}
type CookieConf struct {
Enabled bool `koanf:"enabled" json:"enabled"`
Name string `koanf:"name" json:"name"`
Email string `koanf:"email" json:"email"`
Link string `koanf:"link" json:"link"`
Password string `koanf:"password" json:"password"`
BadgeName string `koanf:"badge_name" json:"badge_name"`
BadgeColor string `koanf:"badge_color" json:"badge_color"`
ReceiveEmail *bool `koanf:"receive_email" json:"receive_email"`
}
type ModeratorConf struct {
@@ -257,7 +252,7 @@ type RedisConf struct {
}
type IPRegionConf struct {
Enabled bool `koanf:"enabled" json:"enabled"` // 启用 IP 归属地展示
Enabled bool `koanf:"enabled" json:"enabled"` // 启用 IP 属地展示
DBPath string `koanf:"db_path" json:"db_path"` // 数据文件路径
Precision string `koanf:"precision" json:"precision"` // 显示精度
}
@@ -297,6 +292,7 @@ type AdminNotifyConf struct {
Slack NotifySlackConf `koanf:"slack" json:"slack"` // slack
LINE NotifyLINEConf `koanf:"line" json:"line"` // LINE
WebHook NotifyWebHookConf `koanf:"webhook" json:"webhook"` // WebHook
NotifyPending bool `koanf:"notify_pending" json:"notify_pending"` // 待审核评论通知
NoiseMode bool `koanf:"noise_mode" json:"noise_mode"` // 嘈杂模式 (非回复管理员的评论也发送通知)
}
-11
View File
@@ -14,16 +14,5 @@ var CONF_DEFAULT_FILENAMES = [...]string{
"conf.yml",
}
// 所有站点
const ATK_SITE_ALL = "__ATK_SITE_ALL"
// Cookie 键
const COOKIE_KEY_ATK_AUTH = "ATK_AUTH"
// ctx keys
const CTX_KEY_ATK_SITE_ID = "atk_site_id"
const CTX_KEY_ATK_SITE_NAME = "atk_site_name"
const CTX_KEY_ATK_SITE_ALL = "atk_site_all"
// 图片上传目录路由重写路径
const IMG_UPLOAD_PUBLIC_PATH = "/static/images"
+11 -4
View File
@@ -23,14 +23,16 @@ type App struct {
cache *cache.Cache
service *map[string]Service
onTerminate *hook.Hook[*TerminateEvent]
onTerminate *hook.Hook[*TerminateEvent]
onConfUpdated *hook.Hook[*ConfUpdatedEvent]
}
func NewApp(conf *config.Config) *App {
app := &App{
conf: conf,
service: &map[string]Service{},
onTerminate: &hook.Hook[*TerminateEvent]{},
conf: conf,
service: &map[string]Service{},
onTerminate: &hook.Hook[*TerminateEvent]{},
onConfUpdated: &hook.Hook[*ConfUpdatedEvent]{},
}
app.injectDefaultServices()
@@ -189,6 +191,7 @@ func (app *App) Conf() *config.Config {
func (app *App) SetConf(conf *config.Config) {
app.conf = conf
app.onConfUpdated.Trigger(&ConfUpdatedEvent{App: app, Conf: conf})
}
func (app *App) Dao() *dao.Dao {
@@ -274,3 +277,7 @@ func (app *App) initDao() error {
func (app *App) OnTerminate() *hook.Hook[*TerminateEvent] {
return app.onTerminate
}
func (app *App) OnConfUpdated() *hook.Hook[*ConfUpdatedEvent] {
return app.onConfUpdated
}
-4
View File
@@ -1,8 +1,6 @@
package core
import (
"strings"
"github.com/ArtalkJS/Artalk/internal/entity"
)
@@ -29,7 +27,6 @@ func (app *App) syncFromConf() {
IsAdmin: true,
IsInConf: true,
ReceiveEmail: receiveEmail,
SiteNames: strings.Join(admin.Sites, ","),
}
app.dao.CreateUser(&user)
} else {
@@ -43,7 +40,6 @@ func (app *App) syncFromConf() {
user.IsAdmin = true
user.IsInConf = true
user.ReceiveEmail = receiveEmail
user.SiteNames = strings.Join(admin.Sites, ",")
app.dao.UpdateUser(&user)
}
}
+7
View File
@@ -1,5 +1,7 @@
package core
import "github.com/ArtalkJS/Artalk/internal/config"
// -------------------------------------------------------------------
// Event data
// -------------------------------------------------------------------
@@ -11,3 +13,8 @@ type BootstrapEvent struct {
type TerminateEvent struct {
App *App
}
type ConfUpdatedEvent struct {
App *App
Conf *config.Config
}
+3 -4
View File
@@ -3,6 +3,7 @@ package dao
import (
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/samber/lo"
)
// ===============
@@ -29,9 +30,11 @@ func (dao *Dao) CookComment(c *entity.Comment) entity.CookedComment {
IsPending: c.IsPending,
IsPinned: c.IsPinned,
IsAllowReply: c.IsAllowReply(),
IsVerified: lo.If(user.IsAdmin, true).Else(c.IsVerified),
Rid: c.Rid,
BadgeName: user.BadgeName,
BadgeColor: user.BadgeColor,
IP: c.IP,
Visible: true,
VoteUp: c.VoteUp,
VoteDown: c.VoteDown,
@@ -178,8 +181,6 @@ func (dao *Dao) FindAllSitesCooked() []entity.CookedSite {
// ===============
func (dao *Dao) CookUser(u *entity.User) entity.CookedUser {
splitSites := utils.SplitAndTrimSpace(u.SiteNames, ",")
return entity.CookedUser{
ID: u.ID,
Name: u.Name,
@@ -188,8 +189,6 @@ func (dao *Dao) CookUser(u *entity.User) entity.CookedUser {
BadgeName: u.BadgeName,
BadgeColor: u.BadgeColor,
IsAdmin: u.IsAdmin,
SiteNames: splitSites,
SiteNamesRaw: u.SiteNames,
ReceiveEmail: u.ReceiveEmail,
}
}
+4 -9
View File
@@ -219,16 +219,11 @@ func (dao *Dao) GetVoteNumUpDown(targetID uint, voteTo string) (int, int) {
//#endregion
// #region 管理员账号检测
var allAdmins *[]entity.User = nil
func (dao *Dao) GetAllAdmins() []entity.User {
if allAdmins == nil {
var admins []entity.User
dao.DB().Where(&entity.User{IsAdmin: true}).Find(&admins)
allAdmins = &admins
}
return *allAdmins
// TODO add cache and flush cache when admin changed
var admins []entity.User
dao.DB().Where(&entity.User{IsAdmin: true}).Find(&admins)
return admins
}
func (dao *Dao) GetAllAdminIDs() []uint {
+24 -5
View File
@@ -1,7 +1,11 @@
package dao
import (
"fmt"
"strings"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/utils"
)
func (dao *Dao) FindCreateSite(siteName string) entity.Site {
@@ -20,12 +24,27 @@ func (dao *Dao) FindCreatePage(pageKey string, pageTitle string, siteName string
return page
}
func (dao *Dao) FindCreateUser(name string, email string, link string) entity.User {
user := dao.FindUser(name, email)
if user.IsEmpty() {
user = dao.NewUser(name, email, link) // save a new user
func (dao *Dao) FindCreateUser(name string, email string, link string) (user entity.User, err error) {
name = strings.TrimSpace(name)
email = strings.TrimSpace(email)
link = strings.TrimSpace(link)
if name == "" || email == "" {
return entity.User{}, fmt.Errorf("name and email are required")
}
return user
if !utils.ValidateEmail(email) {
return entity.User{}, fmt.Errorf("email is invalid")
}
if link != "" && !utils.ValidateURL(link) {
link = ""
}
user = dao.FindUser(name, email)
if user.IsEmpty() {
user, err = dao.NewUser(name, email, link) // save a new user
if err != nil {
return entity.User{}, err
}
}
return user, nil
}
func (dao *Dao) FindCreateNotify(userID uint, lookCommentID uint) entity.Notify {
+28 -2
View File
@@ -70,7 +70,8 @@ func TestFindCreateUser(t *testing.T) {
userLink = "https://qwqaq.com"
)
result := app.Dao().FindCreateUser(userName, userEmail, userLink)
result, err := app.Dao().FindCreateUser(userName, userEmail, userLink)
assert.NoError(t, err)
assert.False(t, result.IsEmpty())
assert.Equal(t, userName, result.Name)
assert.Equal(t, userEmail, result.Email)
@@ -81,8 +82,33 @@ func TestFindCreateUser(t *testing.T) {
assert.Equal(t, app.Dao().CookUser(&result), app.Dao().CookUser(&findUser), "创建后的用户数据有问题")
})
t.Run("Valid User Values", func(t *testing.T) {
args := []struct {
name string
email string
link string
result bool
}{
{"", "", "", false},
{"userA", "", "", false},
{"", "user_a@example.com", "", false},
{"userB", "user_b", "", false},
{"userC", "user_c@example.com", "https://xxxx.com", true},
}
for _, arg := range args {
_, err := app.Dao().FindCreateUser(arg.name, arg.email, arg.link)
assert.Equal(t, arg.result, err == nil, "FindCreateUser(%s, %s, %s) should return %v", arg.name, arg.email, arg.link, arg.result)
}
// Invalid user link
u, err := app.Dao().FindCreateUser("userD", "user_d@example.com", "xxxx.com")
assert.NoError(t, err)
assert.Equal(t, "", u.Link, "The user should be create but link is empty because it's invalid")
})
t.Run("Find Existed User", func(t *testing.T) {
result := app.Dao().FindCreateUser("userA", "user_a@qwqaq.com", "")
result, err := app.Dao().FindCreateUser("userA", "user_a@qwqaq.com", "")
assert.NoError(t, err)
assert.False(t, result.IsEmpty())
assert.Equal(t, app.Dao().FindUser("userA", "user_a@qwqaq.com"), result)
})
+42 -3
View File
@@ -3,6 +3,7 @@ package dao_test
import (
"testing"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/test"
"github.com/stretchr/testify/assert"
)
@@ -88,7 +89,6 @@ func TestFindUser(t *testing.T) {
assert.Equal(t, "admin@qwqaq.com", result.Email)
assert.Equal(t, "123456", result.Password)
assert.Equal(t, true, result.IsAdmin)
assert.Equal(t, "", result.SiteNames)
assert.Equal(t, "管理员", result.BadgeName)
})
@@ -110,7 +110,6 @@ func TestFindUserByID(t *testing.T) {
assert.Equal(t, "admin@qwqaq.com", result.Email)
assert.Equal(t, "123456", result.Password)
assert.Equal(t, true, result.IsAdmin)
assert.Equal(t, "", result.SiteNames)
assert.Equal(t, "管理员", result.BadgeName)
})
@@ -205,7 +204,47 @@ func TestGetAllAdmins(t *testing.T) {
defer app.Cleanup()
allAdmins := app.Dao().GetAllAdmins()
assert.GreaterOrEqual(t, len(allAdmins), 1)
assert.GreaterOrEqual(t, len(allAdmins), 1, "GetAllAdmins() not works")
t.Run("Test modify and get admins", func(t *testing.T) {
getContainsUser := func(userID uint) bool {
for _, a := range app.Dao().GetAllAdmins() {
if a.ID == userID {
return true
}
}
return false
}
// create
var adminID uint = 0
admin := entity.User{
Name: "TestAdmin",
Email: "admin@test.com",
IsAdmin: true,
}
app.Dao().CreateUser(&admin)
adminID = admin.ID
assert.NotZero(t, adminID, "user create failed")
assert.Equal(t, true, getContainsUser(adminID), "admin not found after create")
// update
admin.IsAdmin = false
app.Dao().UpdateUser(&admin)
assert.Equal(t, false, getContainsUser(adminID), "admin still exists after update")
// re-update
admin.IsAdmin = true
app.Dao().UpdateUser(&admin)
assert.Equal(t, true, getContainsUser(adminID), "admin not found after re-update to admin")
// delete
app.Dao().DelUser(&admin)
// not contains admin
assert.Equal(t, false, getContainsUser(adminID), "admin still exists after delete")
})
}
func TestIsAdminUser(t *testing.T) {
+3 -2
View File
@@ -33,7 +33,7 @@ func (dao *Dao) CreateSite(site *entity.Site) error {
return nil
}
func (dao *Dao) NewUser(name string, email string, link string) entity.User {
func (dao *Dao) NewUser(name string, email string, link string) (entity.User, error) {
user := entity.User{
Name: name,
Email: email,
@@ -43,9 +43,10 @@ func (dao *Dao) NewUser(name string, email string, link string) entity.User {
err := dao.CreateUser(&user)
if err != nil {
log.Error("Create User error: ", err)
return entity.User{}, err
}
return user
return user, nil
}
func (dao *Dao) CreateUser(user *entity.User) error {
+7
View File
@@ -17,6 +17,13 @@ func NewDB(conf config.DBConf) (*gorm.DB, error) {
},
}
// Enable Prepared Statement by default
if prepareStmt := conf.PrepareStmt; prepareStmt != nil {
gormConfig.PrepareStmt = *prepareStmt
} else {
gormConfig.PrepareStmt = true
}
var dsn string
if conf.Dsn != "" {
dsn = conf.Dsn
+8 -1
View File
@@ -27,7 +27,14 @@ func OpenMySql(dsn string, gormConfig *gorm.Config) (*gorm.DB, error) {
}
func OpenPostgreSQL(dsn string, gormConfig *gorm.Config) (*gorm.DB, error) {
return gorm.Open(postgres.Open(dsn), gormConfig)
return gorm.Open(postgres.New(postgres.Config{
DSN: dsn,
// gorm v2 use `pgx` as postgres’s database/sql driver,
// it enables prepared statement cache by default,
// disable it when `PrepareStmt` is false by following code:
PreferSimpleProtocol: !gormConfig.PrepareStmt,
}), gormConfig)
}
func OpenSqlServer(dsn string, gormConfig *gorm.Config) (*gorm.DB, error) {
+4 -3
View File
@@ -12,9 +12,10 @@ type Comment struct {
PageKey string `gorm:"index;size:255"`
SiteName string `gorm:"index;size:255"`
UserID uint `gorm:"index"`
UA string
IP string
UserID uint `gorm:"index"`
IsVerified bool `gorm:"default:false"`
UA string
IP string
Rid uint `gorm:"index"` // 父评论 ID
+2
View File
@@ -14,9 +14,11 @@ type CookedComment struct {
IsPending bool `json:"is_pending"`
IsPinned bool `json:"is_pinned"`
IsAllowReply bool `json:"is_allow_reply"`
IsVerified bool `json:"is_verified"`
Rid uint `json:"rid"`
BadgeName string `json:"badge_name"`
BadgeColor string `json:"badge_color"`
IP string `json:"-"`
IPRegion string `json:"ip_region,omitempty"`
Visible bool `json:"visible"`
VoteUp int `json:"vote_up"`
+50 -11
View File
@@ -1,23 +1,29 @@
package entity
import (
"crypto/md5"
"database/sql"
"fmt"
"strings"
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string `gorm:"index;size:255"`
Email string `gorm:"index;size:255"`
Link string
Password string
BadgeName string
BadgeColor string
LastIP string
LastUA string
IsAdmin bool
SiteNames string
ReceiveEmail bool `gorm:"default:true"`
Name string `gorm:"index;size:255"`
Email string `gorm:"index;size:255"`
Link string
Password string
BadgeName string
BadgeColor string
LastIP string
LastUA string
IsAdmin bool
ReceiveEmail bool `gorm:"default:true"`
TokenValidFrom sql.NullTime
// 配置文件中添加的
IsInConf bool
@@ -35,5 +41,38 @@ func (u *User) SetPasswordEncrypt(password string) (err error) {
return err
}
u.Password = "(bcrypt)" + string(encrypted)
u.TokenValidFrom.Scan(time.Now())
return nil
}
func (u *User) CheckPassword(input string) bool {
if u.ID == 0 {
return false
}
password := strings.TrimSpace(u.Password)
if password == "" {
return false
}
const BcryptPrefix = "(bcrypt)"
const MD5Prefix = "(md5)"
switch {
case strings.HasPrefix(password, BcryptPrefix):
if err := bcrypt.CompareHashAndPassword([]byte(password[len(BcryptPrefix):]),
[]byte(input)); err == nil {
return true
}
case strings.HasPrefix(password, MD5Prefix):
if strings.EqualFold(password[len(MD5Prefix):],
fmt.Sprintf("%x", md5.Sum([]byte(input)))) {
return true
}
default:
if password == input {
return true
}
}
return false
}
+8 -10
View File
@@ -1,14 +1,12 @@
package entity
type CookedUser struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Link string `json:"link"`
BadgeName string `json:"badge_name"`
BadgeColor string `json:"badge_color"`
IsAdmin bool `json:"is_admin"`
SiteNames []string `json:"site_names"`
SiteNamesRaw string `json:"site_names_raw"`
ReceiveEmail bool `json:"receive_email"`
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Link string `json:"link"`
BadgeName string `json:"badge_name"`
BadgeColor string `json:"badge_color"`
IsAdmin bool `json:"is_admin"`
ReceiveEmail bool `json:"receive_email"`
}
+7 -2
View File
@@ -1,6 +1,8 @@
package ip_region
import (
"strings"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/log"
)
@@ -21,11 +23,14 @@ func NewIPRegion(conf IPRegionConf) *IPRegion {
}
func (ipRegion *IPRegion) IP2Region(ip string) string {
ip = ipScraper(ip)
if strings.TrimSpace(ip) == "" {
return ""
}
ip = ipScraper(ip)
region, err := search(ip, ipRegion.conf.DBPath, ipRegion.conf.CacheEnabled)
if err != nil {
log.Error(err)
log.Warn("[IP2Region] ", err)
return ""
}
+14 -4
View File
@@ -2,7 +2,6 @@ package notify_pusher
import (
"github.com/ArtalkJS/Artalk/internal/entity"
"golang.org/x/exp/slices"
)
func (pusher *NotifyPusher) checkNeedSendEmailToUser(comment *entity.Comment, parentComment *entity.Comment) bool {
@@ -35,6 +34,11 @@ func (pusher *NotifyPusher) checkNeedSendEmailToAdmin(comment *entity.Comment, p
return false
}
// 待审评论不发送通知
if comment.IsPending && !pusher.conf.NotifyPending {
return false
}
// 管理员自己回复自己,不提醒
if comment.UserID == admin.ID {
return false
@@ -52,9 +56,10 @@ func (pusher *NotifyPusher) checkNeedSendEmailToAdmin(comment *entity.Comment, p
}
// 只发送给对应站点管理员
if admin.SiteNames != "" && !slices.Contains(pusher.dao.CookUser(admin).SiteNames, comment.SiteName) {
return false
}
// TODO admin.SiteNames had removed, so temporarily disabled
// if admin.SiteNames != "" && !slices.Contains(pusher.dao.CookUser(admin).SiteNames, comment.SiteName) {
// return false
// }
// 该管理员单独设定关闭接收邮件
if !admin.ReceiveEmail {
@@ -73,6 +78,11 @@ func (pusher *NotifyPusher) checkNeedMultiPush(comment *entity.Comment, pComment
return false
}
// 待审评论不发送通知
if comment.IsPending && !pusher.conf.NotifyPending {
return false
}
// 非嘈杂模式
if !pusher.conf.NoiseMode {
// 如果不是 root 评论 且 回复目标不是管理员,直接忽略
-1
View File
@@ -10,7 +10,6 @@ func (pusher *NotifyPusher) sendEmail(notify *entity.Notify) {
if pusher.conf.EmailPush != nil {
pusher.conf.EmailPush(notify)
}
// email.AsyncSend(&notify)
}
func (pusher *NotifyPusher) emailToUser(comment *entity.Comment, pComment *entity.Comment) {
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/notify_pusher/sender"
"github.com/ArtalkJS/Artalk/internal/template"
@@ -65,7 +66,7 @@ func (pusher *NotifyPusher) getAdminNotifySubjectBody(comment *entity.Comment, t
body := render.Render(&notify)
if comment.IsPending {
body = "[待审状态评论]\n\n" + body
body = "[" + i18n.T("Pending") + "]\n\n" + body
}
return subject, body
+1 -2
View File
@@ -7,7 +7,6 @@ import (
// 通知发送 (from comment to parentComment)
func (pusher *NotifyPusher) Push(comment *entity.Comment, pComment *entity.Comment) {
isRootComment := pComment == nil || pComment.IsEmpty()
isAdminNoiseModeOn := pusher.conf.NoiseMode
// ==============
// 邮件回复对方
@@ -19,7 +18,7 @@ func (pusher *NotifyPusher) Push(comment *entity.Comment, pComment *entity.Comme
// ==============
// 邮件通知管理员
// ==============
if isRootComment || isAdminNoiseModeOn {
if isRootComment || pusher.conf.NoiseMode {
pusher.emailToAdmins(comment, pComment)
}
+1 -1
View File
@@ -92,7 +92,7 @@ func getInternalTpl(tplType RenderType, tplName string) string {
func getExternalTpl(filename string) (string, error) {
// TODO 反复文件 IO 操作会导致性能下降,之后优化可以改成程序启动时加载模板文件到内存中
// TODO 安全问题:注意这里允许离开工作目录读取文件,
// 由于 filename 是动态可配置项,若管理员凭证泄露,可能会造成文件泄露
// 由于 filename 是动态可配置项,若管理员凭证泄露,可能会造成文件泄露
// (需注意检测 filename 的合法性,建议程序在低权限账户下运行,或丢到 docker 运行)
buf, err := os.ReadFile(filename)
if err != nil {
+16 -16
View File
@@ -14,13 +14,13 @@
"test": "pnpm -F artalk test"
},
"devDependencies": {
"@playwright/test": "^1.40.1",
"@playwright/test": "^1.41.1",
"@rollup/plugin-dynamic-import-vars": "2.1.2",
"@types/node": "20.10.5",
"@typescript-eslint/eslint-plugin": "6.15.0",
"@typescript-eslint/parser": "6.15.0",
"@vitest/coverage-v8": "^1.1.0",
"autoprefixer": "10.4.16",
"@typescript-eslint/eslint-plugin": "6.20.0",
"@typescript-eslint/parser": "6.20.0",
"@vitest/coverage-v8": "^1.2.2",
"autoprefixer": "10.4.17",
"cross-env": "^7.0.3",
"eslint": "8.56.0",
"eslint-config-airbnb-base": "15.0.0",
@@ -29,20 +29,20 @@
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-compat": "4.2.0",
"eslint-plugin-import": "2.29.1",
"jsdom": "^23.0.1",
"postcss": "8.4.32",
"prettier": "3.1.1",
"rollup": "4.9.1",
"sass": "1.69.5",
"stylelint": "^16.1.0",
"terser": "5.26.0",
"jsdom": "^24.0.0",
"postcss": "8.4.33",
"prettier": "3.2.4",
"rollup": "4.9.6",
"sass": "1.70.0",
"stylelint": "^16.2.0",
"terser": "5.27.0",
"tsx": "^4.7.0",
"typescript": "5.3.3",
"vite": "5.0.10",
"vite": "5.0.12",
"vite-plugin-checker": "0.6.2",
"vite-plugin-dts": "3.6.4",
"vite-tsconfig-paths": "4.2.2",
"vitest": "^1.1.0"
"vite-plugin-dts": "3.7.2",
"vite-tsconfig-paths": "4.3.1",
"vitest": "^1.2.2"
},
"browserslist": [
"supports es6 and supports es6-class and supports es6-generators and supports es6-module-dynamic-import and supports es6-module and supports es6-number and supports es6-string-includes"
+887 -1505
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -1,3 +1,3 @@
#!/bin/bash
bombardier -c 125 -d 10s -b "site_name=ArtalkDocs&page_key=https%3A%2F%2Fartalk.js.org%2Fguide%2Fintro.html&limit=20&offset=0" -m POST -H "Origin: http://127.0.0.1:5173" -H "Content-Type: application/x-www-form-urlencoded" http://127.0.0.1:23366/api/get
bombardier -c 125 -d 10s -b "site_name=ArtalkDocs&page_key=https%3A%2F%2Fartalk.js.org%2Fguide%2Fintro.html&limit=20&offset=0" -m GET -H "Origin: http://127.0.0.1:5173" -H "Content-Type: application/x-www-form-urlencoded" http://127.0.0.1:23366/api/v2/comments
+34 -37
View File
@@ -6,7 +6,6 @@ import (
"strings"
"time"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/gofiber/fiber/v2"
@@ -16,21 +15,16 @@ import (
// jwtCustomClaims are custom claims extending default ones.
// See https://github.com/golang-jwt/jwt for more examples
type jwtCustomClaims struct {
UserID uint `json:"user_id"`
Name string `json:"name"`
Email string `json:"email"`
IsAdmin bool `json:"is_admin"`
UserID uint `json:"user_id"`
jwt.StandardClaims
}
func LoginGetUserToken(user entity.User, key string, ttl int) string {
func LoginGetUserToken(user entity.User, key string, ttl int) (string, error) {
// Set custom claims
claims := &jwtCustomClaims{
UserID: user.ID,
Name: user.Name,
Email: user.Email,
IsAdmin: user.IsAdmin,
UserID: user.ID,
StandardClaims: jwt.StandardClaims{
IssuedAt: time.Now().Unix(), // 签发时间
ExpiresAt: time.Now().Add(time.Second * time.Duration(ttl)).Unix(), // 过期时间
},
}
@@ -41,18 +35,17 @@ func LoginGetUserToken(user entity.User, key string, ttl int) string {
// Generate encoded token and send it as response.
t, err := token.SignedString([]byte(key))
if err != nil {
return ""
return "", err
}
return t
return t, nil
}
func GetJwtStrByReqCookie(c *fiber.Ctx) string {
cookie := c.Cookies(config.COOKIE_KEY_ATK_AUTH)
return cookie
}
var ErrTokenNotProvided = fmt.Errorf("token not provided")
var ErrTokenUserNotFound = fmt.Errorf("user not found")
var ErrTokenInvalidFromDate = fmt.Errorf("token is invalid starting from a certain date")
func GetJwtInstanceByReq(app *core.App, c *fiber.Ctx) *jwt.Token {
func GetTokenByReq(c *fiber.Ctx) string {
token := c.Query("token")
if token == "" {
token = c.FormValue("token")
@@ -61,11 +54,13 @@ func GetJwtInstanceByReq(app *core.App, c *fiber.Ctx) *jwt.Token {
token = c.Get(fiber.HeaderAuthorization)
token = strings.TrimPrefix(token, "Bearer ")
}
if token == "" && app.Conf().Cookie.Enabled {
token = GetJwtStrByReqCookie(c)
}
return token
}
func GetJwtDataByReq(app *core.App, c *fiber.Ctx) (jwtCustomClaims, error) {
token := GetTokenByReq(c)
if token == "" {
return nil
return jwtCustomClaims{}, ErrTokenNotProvided
}
jwt, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
@@ -76,29 +71,31 @@ func GetJwtInstanceByReq(app *core.App, c *fiber.Ctx) *jwt.Token {
return []byte(app.Conf().AppKey), nil // 密钥
})
if err != nil {
return nil
}
return jwt
}
func GetUserByJwt(app *core.App, jwt *jwt.Token) entity.User {
if jwt == nil {
return entity.User{}
return jwtCustomClaims{}, err
}
claims := jwtCustomClaims{}
tmp, _ := json.Marshal(jwt.Claims)
_ = json.Unmarshal(tmp, &claims)
return claims, nil
}
func GetUserByReq(app *core.App, c *fiber.Ctx) (entity.User, error) {
claims, err := GetJwtDataByReq(app, c)
if err != nil {
return entity.User{}, err
}
user := app.Dao().FindUserByID(claims.UserID)
if user.IsEmpty() {
return entity.User{}, ErrTokenUserNotFound
}
return user
}
// check tokenValidFrom
if user.TokenValidFrom.Valid && user.TokenValidFrom.Time.After(time.Unix(claims.IssuedAt, 0)) {
return entity.User{}, ErrTokenInvalidFromDate
}
func GetUserByReq(app *core.App, c *fiber.Ctx) entity.User {
jwt := GetJwtInstanceByReq(app, c)
user := GetUserByJwt(app, jwt)
return user
return user, nil
}
+60 -5
View File
@@ -5,23 +5,78 @@ import (
"github.com/ArtalkJS/Artalk/internal/captcha"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/server/middleware/limiter"
"github.com/gofiber/fiber/v2"
)
func GetLimiter[T any](c *fiber.Ctx) (lmt *T, error error) {
l := c.Locals("limiter").(*T)
if l == nil {
return nil, RespError(c, "limiter is not initialize")
func GetLimiter(c *fiber.Ctx) (lmt *limiter.Limiter, err error) {
l, ok := c.Locals("limiter").(*limiter.Limiter)
if l == nil || !ok {
return nil, RespError(c, 500, "limiter is not initialize, but middleware is used")
}
return l, nil
}
func NewCaptchaChecker(app *core.App, c *fiber.Ctx) captcha.Checker {
user, _ := GetUserByReq(app, c)
return captcha.NewCaptchaChecker(&captcha.CheckerConf{
CaptchaConf: app.Conf().Captcha,
User: captcha.User{
ID: fmt.Sprint(GetUserByReq(app, c).ID),
ID: fmt.Sprint(user.ID),
IP: c.IP(),
},
})
}
func LimiterGuard(app *core.App, handler fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
limiter, err := GetLimiter(c)
if err != nil {
return err
}
// 关闭验证码功能,直接 Skip
if !app.Conf().Captcha.Enabled {
return handler(c)
}
// 管理员直接忽略
if CheckIsAdminReq(app, c) {
return handler(c)
}
// 检测是否需要验证码
ip := c.IP()
if limiter.IsPass(ip) {
// 无需验证码
err := handler(c)
if c.Method() != fiber.MethodOptions { // 忽略 Options 请求
limiter.Log(ip) // 记录操作
}
return err
} else {
// create new captcha checker instance
cap := NewCaptchaChecker(app, c)
// response need captcha check
respData := Map{
"need_captcha": true,
}
switch cap.Type() {
case captcha.Image:
// 图片验证码
img, _ := cap.Get()
respData["img_data"] = string(img)
case captcha.IFrame:
// iFrame 验证模式
respData["iframe"] = true
}
return RespError(c, 403, i18n.T("Captcha required"), respData)
}
}
}
+8 -36
View File
@@ -2,52 +2,24 @@ package common
import (
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/gofiber/fiber/v2"
)
func CheckIsAllowed(app *core.App, c *fiber.Ctx, name string, email string, page entity.Page, siteName string) (bool, error) {
isAdminUser := app.Dao().IsAdminUserByNameEmail(name, email)
// 如果用户是管理员,或者当前页只能管理员评论
if isAdminUser || page.AdminOnly {
func AdminGuard(app *core.App, handler fiber.Handler) fiber.Handler {
return func(c *fiber.Ctx) error {
if !CheckIsAdminReq(app, c) {
return false, RespError(c, i18n.T("Admin access required"), Map{"need_login": true})
return RespError(c, 403, i18n.T("Admin access required"), Map{"need_login": true})
}
}
return true, nil
return handler(c)
}
}
func CheckIsAdminReq(app *core.App, c *fiber.Ctx) bool {
jwt := GetJwtInstanceByReq(app, c)
if jwt == nil {
user, err := GetUserByReq(app, c)
if err != nil {
return false
}
user := GetUserByJwt(app, jwt)
return user.IsAdmin
}
func GetIsSuperAdmin(app *core.App, c *fiber.Ctx) bool {
user := GetUserByReq(app, c)
return user.IsAdmin && user.SiteNames == ""
}
func IsAdminHasSiteAccess(app *core.App, c *fiber.Ctx, siteName string) bool {
user := GetUserByReq(app, c)
cookedUser := app.Dao().CookUser(&user)
if !user.IsAdmin {
return false
}
if !GetIsSuperAdmin(app, c) && !utils.ContainsStr(cookedUser.SiteNames, siteName) {
// 如果账户分配了站点,并且待操作的站点并非处于分配的站点列表
return false
}
return true
return !user.IsEmpty() && user.IsAdmin
}
+34 -5
View File
@@ -1,12 +1,15 @@
package common
import (
"fmt"
"strings"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/ArtalkJS/Artalk/server/middleware"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
type ApiVersionData struct {
@@ -23,7 +26,12 @@ func GetApiVersionDataMap() ApiVersionData {
}
}
func GetApiPublicConfDataMap(app *core.App, c *fiber.Ctx) Map {
type ConfData struct {
FrontendConf Map `json:"frontend_conf"`
Version ApiVersionData `json:"version"`
}
func GetApiPublicConfDataMap(app *core.App, c *fiber.Ctx) ConfData {
isAdmin := CheckIsAdminReq(app, c)
imgUpload := app.Conf().ImgUpload.Enabled
if isAdmin {
@@ -43,9 +51,30 @@ func GetApiPublicConfDataMap(app *core.App, c *fiber.Ctx) Map {
frontendConf["locale"] = app.Conf().Locale
}
return Map{
"img_upload": imgUpload,
"frontend_conf": frontendConf,
"version": GetApiVersionDataMap(),
if pluginURLs, ok := frontendConf["pluginURLs"].([]any); ok {
frontendConf["pluginURLs"] = handlePluginURLs(app,
lo.Map[any, string](pluginURLs, func(u any, _ int) string {
return strings.TrimSpace(fmt.Sprintf("%v", u))
}))
}
return ConfData{
FrontendConf: frontendConf,
Version: GetApiVersionDataMap(),
}
}
func handlePluginURLs(app *core.App, urls []string) []string {
return lo.Filter[string](urls, func(u string, _ int) bool {
if strings.TrimSpace(u) == "" {
return false
}
if !utils.ValidateURL(u) {
return true
}
if trusted, _, _ := middleware.CheckURLTrusted(app, u); trusted {
return true
}
return false
})
}
+21
View File
@@ -0,0 +1,21 @@
package common
import (
"errors"
"fmt"
"github.com/gofiber/fiber/v2"
)
func ErrorHandler(ctx *fiber.Ctx, err error) error {
// Status code defaults to 500
code := fiber.StatusInternalServerError
// Retrieve the custom status code if it's a *fiber.Error
var e *fiber.Error
if errors.As(err, &e) {
code = e.Code
}
return RespError(ctx, code, fmt.Sprintf("Error: %s", err.Error()))
}
+53 -43
View File
@@ -1,9 +1,11 @@
package common
import (
"fmt"
"reflect"
"strings"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/gofiber/fiber/v2"
)
@@ -12,74 +14,82 @@ type Map = map[string]interface{}
func ParamsDecode(c *fiber.Ctx, destParams interface{}) (isContinue bool, resp error) {
reqMethod := c.Method()
var errParse error
switch reqMethod {
case "GET":
errParse = c.QueryParser(destParams)
case "POST":
errParse = c.BodyParser(destParams)
// validate required
if isContinue, resp := ValidateRequired(c, destParams); !isContinue {
return false, resp
}
if errParse != nil {
return false, errParse
// parse query
if err := c.QueryParser(destParams); err != nil {
return false, err
}
// parse path params
if err := c.ParamsParser(destParams); err != nil {
return false, err
}
// parse body
if reqMethod == "POST" || reqMethod == "PUT" {
if err := c.BodyParser(destParams); err != nil {
return false, err
}
}
return true, nil
}
func ValidateRequired(c *fiber.Ctx, destParams interface{}) (isContinue bool, resp error) {
reqMethod := c.Method()
reqPath := c.Path()
refVal := reflect.ValueOf(destParams)
for i := 0; i < refVal.Elem().Type().NumField(); i++ {
k := refVal.Elem().Type().Field(i)
// v := refVal.Elem().Field(i)
// fieldName := k.Name
validateTag := k.Tag.Get("validate")
requiredField := (validateTag == "required")
if !requiredField {
continue // 仅 required 检测时才继续执行之后的代码
continue // only check required field
}
paramName := k.Tag.Get("query")
if paramName == "" {
paramName = k.Tag.Get("form")
// get param key
paramKey := ""
tagNames := []string{"query", "json", "form"}
for _, tagName := range tagNames {
paramKey = k.Tag.Get(tagName)
if paramKey != "" {
break
}
}
if paramName == "" {
if paramKey == "" {
log.Errorf("[Validator] %s field '%s' real param key not found", reqPath, k.Name)
continue
}
// get param value
paramVal := func() string {
switch reqMethod {
case "GET":
return c.Query(paramName)
case "POST":
return c.FormValue(paramName)
switch {
case reqMethod == "GET" || reqMethod == "DELETE":
return c.Query(paramKey)
case reqMethod == "POST" || reqMethod == "PUT":
var destMap map[string]interface{}
err := c.BodyParser(&destMap)
if err != nil {
log.Error("[Validator] "+reqPath+" field '"+k.Name+"' ", err)
return ""
}
if v, ok := destMap[paramKey]; ok {
return fmt.Sprintf("%v", v)
}
return ""
}
return ""
}()
// check required param
if requiredField && strings.TrimSpace(paramVal) == "" {
return false, RespError(c, "Param `"+paramName+"` is required")
if strings.TrimSpace(paramVal) == "" {
return false, RespError(c, 400, "Param `"+paramKey+"` is required")
}
// 类型转换交给 fiber 内置 BodyParser 来做,这里不再实现
// convert type
// kind := k.Type.Kind()
// if kind == reflect.String {
// v.SetString(paramVal)
// } else if kind == reflect.Bool {
// v.SetBool((paramVal == "1" || paramVal == "true"))
// } else if (kind == reflect.Int) || (kind == reflect.Uint) {
// u64, err := strconv.ParseInt(paramVal, 10, 32)
// if requiredField && (err != nil || u64 == 0) {
// return false, RespError(c, "Param `"+paramName+"` is required")
// }
// if kind == reflect.Uint {
// v.SetUint(uint64(u64))
// } else {
// v.SetInt(u64)
// }
// }
// // } else if kind == reflect.Array {
// // }
}
return true, nil
+11 -27
View File
@@ -9,38 +9,24 @@ import (
"github.com/gofiber/fiber/v2"
)
// JSONResult JSON 响应数据结构
// JSONResult JSON Response data structure
type JSONResult struct {
Success bool `json:"success"` // 是否成功
Msg string `json:"msg,omitempty"` // 消息
Data interface{} `json:"data,omitempty"` // 数据
Extra interface{} `json:"extra,omitempty"` // 数据
}
// RespJSON is normal json result
func RespJSON(c *fiber.Ctx, msg string, data interface{}, success bool) error {
return c.Status(http.StatusOK).JSON(&JSONResult{
Success: success,
Msg: msg,
Data: data,
})
Msg string `json:"msg,omitempty"` // Message
Data interface{} `json:"data,omitempty"` // Data
}
// RespData is just response data
func RespData(c *fiber.Ctx, data interface{}) error {
return c.Status(http.StatusOK).JSON(&JSONResult{
Success: true,
Data: data,
})
return c.Status(http.StatusOK).JSON(data)
}
// RespSuccess is just response success
func RespSuccess(c *fiber.Ctx, msg ...string) error {
respData := &JSONResult{
Success: true,
Msg: "Success",
}
// 可选参数 msg
// Optional msg parameter
if len(msg) > 0 {
respData.Msg = msg[0]
}
@@ -49,24 +35,22 @@ func RespSuccess(c *fiber.Ctx, msg ...string) error {
}
// RespError is just response error
func RespError(c *fiber.Ctx, msg string, data ...Map) error {
func RespError(c *fiber.Ctx, code int, msg string, data ...Map) error {
// log
path := c.Path()
if path == "" {
path = "/"
}
LogWithHttpInfo(c).Errorf("[响应] %s %s ==> %s", c.Method(), path, strconv.Quote(msg))
LogWithHttpInfo(c).Errorf("[Response] %s %s ==> %s", c.Method(), path, strconv.Quote(msg))
respData := Map{}
if len(data) > 0 {
respData = data[0]
}
return c.Status(http.StatusOK).JSON(&JSONResult{
Success: false,
Msg: msg,
Data: respData,
})
respData["msg"] = msg
return c.Status(code).JSON(respData)
}
func LogWithHttpInfo(c *fiber.Ctx) *log.Entry {
+28 -9
View File
@@ -1,18 +1,37 @@
package common
import (
"github.com/ArtalkJS/Artalk/internal/config"
"strings"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/gofiber/fiber/v2"
)
func UseSite(c *fiber.Ctx, siteName *string, destID *uint, destSiteAll *bool) {
if destID != nil {
*destID = c.Locals(config.CTX_KEY_ATK_SITE_ID).(uint)
func CheckSiteExist(app *core.App, c *fiber.Ctx, siteName string) (entity.CookedSite, bool, error) {
siteName = strings.TrimSpace(siteName)
if siteName == "" {
return entity.CookedSite{}, false, RespError(c,
400,
i18n.T("{{name}} cannot be empty", Map{"name": i18n.T("Site name")}),
Map{
"err_no_site": true,
},
)
}
if siteName != nil {
*siteName = c.Locals(config.CTX_KEY_ATK_SITE_NAME).(string)
}
if destSiteAll != nil {
*destSiteAll = c.Locals(config.CTX_KEY_ATK_SITE_ALL).(bool)
findSite := app.Dao().FindSite(siteName)
if findSite.IsEmpty() {
return entity.CookedSite{}, false, RespError(c,
404,
i18n.T("Site `{{name}}` not found. Please create it in control center.", map[string]interface{}{"name": siteName}),
Map{
"err_no_site": true,
},
)
}
return app.Dao().CookSite(&findSite), true, nil
}
-82
View File
@@ -1,82 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminCacheWarm struct {
}
// @Summary Cache Warming
// @Description Cache warming helps you hit the cache on the user's first visit
// @Tags Cache
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/cache-warm [post]
func AdminCacheWarm(app *core.App, router fiber.Router) {
router.Post("/cache-warm", func(c *fiber.Ctx) error {
var p ParamsAdminCacheWarm
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
if !common.GetIsSuperAdmin(app, c) {
return common.RespError(c, i18n.T("Access denied"))
}
if !app.Conf().Cache.Enabled {
return common.RespError(c, "cache disabled")
}
go func() {
app.Dao().CacheWarmUp()
}()
return common.RespData(c, common.Map{
"msg": i18n.T("Task executing in background, please wait..."),
})
})
}
type ParamsAdminCacheFlush struct {
FlushAll bool `form:"flush_all"`
}
// @Summary Cache Flush
// @Description Flush Cache when application runs
// @Tags Cache
// @Param flush_all formData int false "flush all cache" example(1)
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/cache-flush [post]
func AdminCacheFlush(app *core.App, router fiber.Router) {
router.Post("/cache-flush", func(c *fiber.Ctx) error {
var p ParamsAdminCacheFlush
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
if !common.GetIsSuperAdmin(app, c) {
return common.RespError(c, i18n.T("Access denied"))
}
if !app.Conf().Cache.Enabled {
return common.RespError(c, "cache disabled")
}
if p.FlushAll {
go func() {
app.Dao().CacheFlushAll()
}()
return common.RespData(c, common.Map{
"msg": i18n.T("Task executing in background, please wait..."),
})
}
return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Parameter")}))
})
}
-58
View File
@@ -1,58 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsCommentDel struct {
ID uint `form:"id" validate:"required"`
SiteName string
SiteID uint
SiteAll bool
}
// @Summary Comment Delete
// @Description Delete a specific comment
// @Tags Comment
// @Param id formData int true "the comment ID you want to delete"
// @Param site_name formData string false "the site name of your content scope"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/comment-del [post]
func AdminCommentDel(app *core.App, router fiber.Router) {
router.Post("/comment-del", func(c *fiber.Ctx) error {
var p ParamsCommentDel
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
// use site
common.UseSite(c, &p.SiteName, &p.SiteID, &p.SiteAll)
// find comment
comment := app.Dao().FindComment(p.ID)
if comment.IsEmpty() {
return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Comment")}))
}
if !common.IsAdminHasSiteAccess(app, c, comment.SiteName) {
return common.RespError(c, i18n.T("Access denied"))
}
// 删除主评论
if err := app.Dao().DelComment(&comment); err != nil {
return common.RespError(c, i18n.T("{{name}} deletion failed", Map{"name": i18n.T("Comment")}))
}
// 删除子评论
if err := app.Dao().DelCommentChildren(comment.ID); err != nil {
return common.RespError(c, i18n.T("{{name}} deletion failed", Map{"name": i18n.T("Sub-comment")}))
}
return common.RespSuccess(c)
})
}
-192
View File
@@ -1,192 +0,0 @@
package handler
import (
"fmt"
"strconv"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsCommentEdit struct {
// 查询值
ID uint `form:"id" validate:"required"`
SiteName string
SiteID uint
SiteAll bool
// 可修改
Content string `form:"content"`
PageKey string `form:"page_key"`
Nick string `form:"nick"`
Email string `form:"email"`
Link string `form:"link"`
Rid string `form:"rid"`
UA string `form:"ua"`
IP string `form:"ip"`
IsCollapsed bool `form:"is_collapsed"`
IsPending bool `form:"is_pending"`
IsPinned bool `form:"is_pinned"`
}
type ResponseCommentEdit struct {
Comment entity.CookedComment `json:"comment"`
}
// @Summary Comment Edit
// @Description Edit a specific comment
// @Tags Comment
// @Param id formData int true "the comment ID you want to edit"
// @Param site_name formData string false "the site name of your content scope"
// @Param content formData string false "the comment content"
// @Param page_key formData string false "the comment page_key"
// @Param nick formData string false "the comment nick"
// @Param email formData string false "the comment email"
// @Param link formData string false "the comment link"
// @Param rid formData string false "the comment rid"
// @Param ua formData string false "the comment ua"
// @Param ip formData string false "the comment ip"
// @Param is_collapsed formData bool false "the comment is_collapsed"
// @Param is_pending formData bool false "the comment is_pending"
// @Param is_pinned formData bool false "the comment is_pinned"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=ResponseCommentEdit}
// @Router /admin/comment-edit [post]
func AdminCommentEdit(app *core.App, router fiber.Router) {
router.Post("/comment-edit", func(c *fiber.Ctx) error {
var p ParamsCommentEdit
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
// use site
common.UseSite(c, &p.SiteName, &p.SiteID, &p.SiteAll)
// find comment
comment := app.Dao().FindComment(p.ID)
if comment.IsEmpty() {
return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Comment")}))
}
if !common.IsAdminHasSiteAccess(app, c, comment.SiteName) {
return common.RespError(c, i18n.T("Access denied"))
}
// check params
if p.Email != "" && !utils.ValidateEmail(p.Email) {
return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Email")}))
}
if p.Link != "" && !utils.ValidateURL(p.Link) {
return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Link")}))
}
// content
if p.Content != "" {
comment.Content = p.Content
}
// rid
if p.Rid != "" {
if rid, err := strconv.Atoi(p.Rid); err == nil {
comment.Rid = uint(rid)
}
}
// merge user
originalUser := app.Dao().FetchUserForComment(&comment)
if p.Nick == "" {
p.Nick = originalUser.Name
}
if p.Email == "" {
p.Email = originalUser.Email
}
// find or save new user
user := app.Dao().FindCreateUser(p.Nick, p.Email, p.Link)
if user.ID != comment.UserID {
comment.UserID = user.ID
}
// user link update
if p.Link != "" && p.Link != user.Link {
user.Link = p.Link
app.Dao().UpdateUser(&user)
}
// pageKey
if p.PageKey != "" && p.PageKey != comment.PageKey {
app.Dao().FindCreatePage(p.PageKey, "", p.SiteName)
comment.PageKey = p.PageKey
}
if p.UA != "" {
comment.UA = p.UA
}
if p.IP != "" {
comment.IP = p.IP
}
comment.IsCollapsed = p.IsCollapsed
comment.IsPinned = p.IsPinned
if p.IsPending != comment.IsPending {
// 待审状态发生改变
comment.IsPending = p.IsPending
// 待审状态被修改为 false,则重新发送邮件通知
if !comment.IsPending {
if err := RenotifyWhenPendingModified(app, &comment); err != nil {
log.Error("[RenotifyWhenPendingModified] error: ", err)
return common.RespError(c, "Renotify Err: "+err.Error())
}
}
}
if err := app.Dao().UpdateComment(&comment); err != nil {
return common.RespError(c, i18n.T("{{name}} save failed", Map{"name": i18n.T("Comment")}))
}
return common.RespData(c, ResponseCommentEdit{
Comment: app.Dao().CookComment(&comment),
})
})
}
func RenotifyWhenPendingModified(app *core.App, comment *entity.Comment) (err error) {
if comment.Rid == 0 {
return // Root 评论不发送通知,因为这个评论已经被管理员看到了
}
pComment := app.Dao().FindComment(comment.Rid)
if app.Dao().FetchUserForComment(&pComment).IsAdmin {
return // 回复对象是管理员,则不再发送通知,因为已经看到了
}
if comment.UserID == pComment.UserID {
return // 自己回复自己,不通知
}
notify := app.Dao().FindCreateNotify(pComment.UserID, comment.ID)
if notify.IsEmailed {
return // 邮件已经发送过,则不再重复发送
}
// 设置通知为未读状态
if err := app.Dao().NotifySetInitial(&notify); err != nil {
return fmt.Errorf("func NotifySetInitial err: %w", err)
}
// 邮件通知
emailService, err := core.AppService[*core.EmailService](app)
if err != nil {
return err
}
emailService.AsyncSend(&notify)
return
}
-50
View File
@@ -1,50 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminPageDel struct {
Key string `form:"key" validate:"required"`
SiteName string
SiteID uint
}
// @Summary Page Delete
// @Description Delete a specific page
// @Tags Page
// @Param key formData string true "the page KEY you want to delete"
// @Param site_name formData string false "the site name of your content scope"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/page-del [post]
func AdminPageDel(app *core.App, router fiber.Router) {
router.Post("/page-del", func(c *fiber.Ctx) error {
var p ParamsAdminPageDel
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
// use site
common.UseSite(c, &p.SiteName, &p.SiteID, nil)
page := app.Dao().FindPage(p.Key, p.SiteName)
if page.IsEmpty() {
return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")}))
}
if !common.IsAdminHasSiteAccess(app, c, page.SiteName) {
return common.RespError(c, i18n.T("Access denied"))
}
err := app.Dao().DelPage(&page)
if err != nil {
return common.RespError(c, i18n.T("{{name}} deletion failed", Map{"name": i18n.T("Page")}))
}
return common.RespSuccess(c)
})
}
-99
View File
@@ -1,99 +0,0 @@
package handler
import (
"strings"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/dao"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminPageEdit struct {
// 查询值
ID uint `form:"id"`
SiteName string
SiteID uint
// 修改值
Key string `form:"key"`
Title string `form:"title"`
AdminOnly bool `form:"admin_only"`
}
type ResponseAdminPageEdit struct {
Page entity.CookedPage `json:"page"`
}
// @Summary Page Edit
// @Description Edit a specific page
// @Tags Page
// @Param id formData string true "the page ID you want to edit"
// @Param site_name formData string false "the site name of your content scope"
// @Param key formData string false "edit page key"
// @Param title formData string false "edit page title"
// @Param admin_only formData bool false "edit page admin_only option"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=ResponseAdminPageEdit}
// @Router /admin/page-edit [post]
func AdminPageEdit(app *core.App, router fiber.Router) {
router.Post("/page-edit", func(c *fiber.Ctx) error {
var p ParamsAdminPageEdit
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
if strings.TrimSpace(p.Key) == "" {
return common.RespError(c, i18n.T("{{name}} cannot be empty", Map{"name": "key"}))
}
// use site
common.UseSite(c, &p.SiteName, &p.SiteID, nil)
// find page
var page = app.Dao().FindPageByID(p.ID)
if page.IsEmpty() {
return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")}))
}
if !common.IsAdminHasSiteAccess(app, c, page.SiteName) {
return common.RespError(c, i18n.T("Access denied"))
}
// 重命名合法性检测
modifyKey := p.Key != page.Key
if modifyKey && !app.Dao().FindPage(p.Key, p.SiteName).IsEmpty() {
return common.RespError(c, i18n.T("{{name}} already exists", Map{"name": i18n.T("Page")}))
}
// 预先删除缓存,防止修改主键原有 page_key 占用问题
app.Dao().CacheAction(func(cache *dao.DaoCache) {
cache.PageCacheDel(&page)
})
page.Title = p.Title
page.AdminOnly = p.AdminOnly
if modifyKey {
// 相关性数据修改
var comments []entity.Comment
app.Dao().DB().Where("page_key = ?", page.Key).Find(&comments)
for _, comment := range comments {
comment.PageKey = p.Key
app.Dao().UpdateComment(&comment)
}
page.Key = p.Key
}
if err := app.Dao().UpdatePage(&page); err != nil {
return common.RespError(c, i18n.T("{{name}} save failed", Map{"name": i18n.T("Page")}))
}
return common.RespData(c, ResponseAdminPageEdit{
Page: app.Dao().CookPage(&page),
})
})
}
-106
View File
@@ -1,106 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminPageFetch struct {
ID uint `form:"id"`
SiteName string
GetStatus bool `form:"get_status"`
}
var allPageFetching = false
var allPageFetchDone = 0
var allPageFetchTotal = 0
// @Summary Page Data Fetch
// @Description Fetch the data of a specific page
// @Tags Page
// @Param key formData string true "the page ID you want to fetch"
// @Param site_name formData string false "the site name of your content scope"
// @Param get_status formData bool false "which response data you want to receive"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/page-fetch [post]
func AdminPageFetch(app *core.App, router fiber.Router) {
router.Post("/page-fetch", func(c *fiber.Ctx) error {
var p ParamsAdminPageFetch
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
common.UseSite(c, &p.SiteName, nil, nil)
// 状态获取
if p.GetStatus {
if allPageFetching {
return common.RespData(c, common.Map{
"msg": i18n.T("{{done}} of {{total}} done", Map{"done": allPageFetchDone, "total": allPageFetchTotal}),
"is_progress": true,
})
} else {
return common.RespData(c, common.Map{
"msg": "",
"is_progress": false,
})
}
}
// 更新全部站点
if p.SiteName != "" {
if allPageFetching {
return common.RespError(c, i18n.T("Task in progress, please wait a moment"))
}
// 异步执行
go func() {
allPageFetching = true
allPageFetchDone = 0
allPageFetchTotal = 0
var pages []entity.Page
db := app.Dao().DB().Model(&entity.Page{})
if p.SiteName != config.ATK_SITE_ALL {
db = db.Where(&entity.Page{SiteName: p.SiteName})
}
db.Find(&pages)
allPageFetchTotal = len(pages)
for _, p := range pages {
if err := app.Dao().FetchPageFromURL(&p); err != nil {
log.Error(c, "[api_admin_page_fetch] page fetch error: "+err.Error())
} else {
allPageFetchDone++
}
}
allPageFetching = false
}()
return common.RespSuccess(c)
}
page := app.Dao().FindPageByID(p.ID)
if page.IsEmpty() {
return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Page")}))
}
if !common.IsAdminHasSiteAccess(app, c, page.SiteName) {
return common.RespError(c, i18n.T("Access denied"))
}
if err := app.Dao().FetchPageFromURL(&page); err != nil {
return common.RespError(c, i18n.T("Page fetch failed")+": "+err.Error())
}
return common.RespData(c, common.Map{
"page": app.Dao().CookPage(&page),
})
})
}
-74
View File
@@ -1,74 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminPageGet struct {
SiteName string
SiteID uint
SiteAll bool
Limit int `form:"limit"`
Offset int `form:"offset"`
}
type ResponseAdminPageGet struct {
Total int64 `json:"total"`
Pages []entity.CookedPage `json:"pages"`
}
// @Summary Page List
// @Description Get a list of pages by some conditions
// @Tags Page
// @Param site_name formData string false "the site name of your content scope"
// @Param limit formData int false "the limit for pagination"
// @Param offset formData int false "the offset for pagination"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=ResponseAdminPageGet}
// @Router /admin/page-get [post]
func AdminPageGet(app *core.App, router fiber.Router) {
router.Post("/page-get", func(c *fiber.Ctx) error {
var p ParamsAdminPageGet
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
// use site
common.UseSite(c, &p.SiteName, &p.SiteID, &p.SiteAll)
if !common.IsAdminHasSiteAccess(app, c, p.SiteName) {
return common.RespError(c, i18n.T("Access denied"))
}
// 准备 query
q := app.Dao().DB().Model(&entity.Page{}).Order("created_at DESC")
if !p.SiteAll { // 不是查的所有站点
q = q.Where("site_name = ?", p.SiteName)
}
// 总共条数
var total int64
q.Count(&total)
// 数据分页
q = q.Scopes(Paginate(p.Offset, p.Limit))
// 查找
var pages []entity.Page
q.Find(&pages)
var cookedPages []entity.CookedPage
for _, p := range pages {
cookedPages = append(cookedPages, app.Dao().CookPage(&p))
}
return common.RespData(c, ResponseAdminPageGet{
Pages: cookedPages,
Total: total,
})
})
}
-46
View File
@@ -1,46 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminSendMail struct {
Subject string `form:"subject" validate:"required"`
Body string `form:"body" validate:"required"`
ToAddr string `form:"to_addr" validate:"required"`
}
// @Summary Email Send
// @Description Send an email to test the email sender
// @Tags System
// @Param subject formData string true "the subject of email"
// @Param body formData string true "the body of email"
// @Param to_addr formData string true "the email address of the receiver"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/send-mail [post]
func AdminSendMail(app *core.App, router fiber.Router) {
router.Post("/send-mail", func(c *fiber.Ctx) error {
var p ParamsAdminSendMail
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
if !common.GetIsSuperAdmin(app, c) {
return common.RespError(c, i18n.T("Access denied"))
}
emailService, err := core.AppService[*core.EmailService](app)
if err != nil {
log.Error("[EmailService] err: ", err)
return common.RespError(c, "EmailService err: "+err.Error())
}
emailService.AsyncSendTo(p.Subject, p.Body, p.ToAddr)
return common.RespSuccess(c)
})
}
-111
View File
@@ -1,111 +0,0 @@
package handler
import (
"net/http"
"os"
"strings"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ResponseAdminSettingGet struct {
Custom string `json:"custom"`
Template string `json:"template"`
}
// @Summary Settings Get
// @Description Get settings from app config file
// @Tags System
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=ResponseAdminSettingGet}
// @Router /admin/setting-get [post]
func AdminSettingGet(app *core.App, router fiber.Router) {
router.Post("/setting-get", func(c *fiber.Ctx) error {
if !common.GetIsSuperAdmin(app, c) {
return common.RespError(c, i18n.T("Access denied"))
}
dat, err := os.ReadFile(app.Conf().GetCfgFileLoaded())
if err != nil {
return common.RespError(c, i18n.T("Config file read failed"))
}
return common.RespData(c, ResponseAdminSettingGet{
Custom: string(dat),
Template: app.ConfTpl(),
})
})
}
type ParamsAdminSettingSave struct {
Data string `form:"data" validate:"required"`
}
// @Summary Settings Save
// @Description Save settings to app config file
// @Tags System
// @Param data formData string true "the content of the config file in YAML format"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/setting-save [post]
func AdminSettingSave(app *core.App, router fiber.Router) {
router.Post("/setting-save", func(c *fiber.Ctx) error {
if !common.GetIsSuperAdmin(app, c) {
return common.RespError(c, i18n.T("Access denied"))
}
var p ParamsAdminSettingSave
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
configFile := app.Conf().GetCfgFileLoaded()
f, err := os.Create(configFile)
if err != nil {
return common.RespError(c, i18n.T("Config file read failed")+": "+err.Error())
}
defer f.Close()
_, err2 := f.WriteString(p.Data)
if err2 != nil {
return common.RespError(c, i18n.T("Save failed")+": "+err2.Error())
}
// 应用新配置文件
conf, err := config.NewFromFile(configFile)
if err != nil {
return common.RespError(c, "Config instance err: "+err.Error())
}
app.SetConf(conf)
// 重启服务
if err := app.Restart(); err != nil {
return common.RespError(c, i18n.T("Restart failed: {{err}}", map[string]interface{}{"err": err.Error()}))
}
log.Info(i18n.T("Services restart complete"))
return common.RespSuccess(c)
})
}
// @Summary Settings Template
// @Description Get config templates in different languages for rendering the settings page in the frontend
// @Tags System
// @Security ApiKeyAuth
// @Success 200 {object} string
// @Router /admin/setting-tpl [post]
func AdminSettingTpl(app *core.App, router fiber.Router) {
router.Post("/setting-tpl", func(c *fiber.Ctx) error {
locale := strings.TrimSpace(c.FormValue("locale"))
tpl := config.Template(locale)
return c.Status(http.StatusOK).SendString(tpl)
})
}
-70
View File
@@ -1,70 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminSiteAdd struct {
Name string `form:"name" validate:"required"`
Urls string `form:"urls"`
}
type ResponseAdminSiteAdd struct {
Site entity.CookedSite `json:"site"`
}
// @Summary Site Add
// @Description Create a new site
// @Tags Site
// @Param name formData string false "the site name"
// @Param urls formData string false "the site urls"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=ResponseAdminSiteAdd}
// @Router /admin/site-add [post]
func AdminSiteAdd(app *core.App, router fiber.Router) {
router.Post("/site-add", func(c *fiber.Ctx) error {
var p ParamsAdminSiteAdd
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
if !common.GetIsSuperAdmin(app, c) {
return common.RespError(c, i18n.T("Access denied"))
}
if p.Urls != "" {
urls := utils.SplitAndTrimSpace(p.Urls, ",")
for _, url := range urls {
if !utils.ValidateURL(url) {
return common.RespError(c, i18n.T("Contains invalid URL"))
}
}
}
if p.Name == config.ATK_SITE_ALL {
return common.RespError(c, "Prohibit the use of reserved keywords as names")
}
if !app.Dao().FindSite(p.Name).IsEmpty() {
return common.RespError(c, i18n.T("{{name}} already exists", Map{"name": i18n.T("Site")}))
}
site := entity.Site{}
site.Name = p.Name
site.Urls = p.Urls
err := app.Dao().CreateSite(&site)
if err != nil {
return common.RespError(c, i18n.T("{{name}} creation failed", Map{"name": i18n.T("Site")}))
}
return common.RespData(c, ResponseAdminSiteAdd{
Site: app.Dao().CookSite(&site),
})
})
}
-44
View File
@@ -1,44 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminSiteDel struct {
ID uint `form:"id" validate:"required"`
}
// @Summary Site Delete
// @Description Delete a specific site
// @Tags Site
// @Param id formData string true "the site ID you want to delete"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/site-del [post]
func AdminSiteDel(app *core.App, router fiber.Router) {
router.Post("/site-del", func(c *fiber.Ctx) error {
var p ParamsAdminSiteDel
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
if !common.GetIsSuperAdmin(app, c) {
return common.RespError(c, i18n.T("Access denied"))
}
site := app.Dao().FindSiteByID(p.ID)
if site.IsEmpty() {
return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Site")}))
}
err := app.Dao().DelSite(&site)
if err != nil {
return common.RespError(c, i18n.T("{{name}} deletion failed", Map{"name": i18n.T("Site")}))
}
return common.RespSuccess(c)
})
}
-109
View File
@@ -1,109 +0,0 @@
package handler
import (
"strings"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/dao"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminSiteEdit struct {
// 查询值
ID uint `form:"id" validate:"required"`
// 修改值
Name string `form:"name"`
Urls string `form:"urls"`
}
type ResponseAdminSiteEdit struct {
Site entity.CookedSite `json:"site"`
}
// @Summary Site Edit
// @Description Edit a specific site
// @Tags Site
// @Param id formData string true "the site ID you want to edit"
// @Param name formData string false "edit site name"
// @Param urls formData string false "edit site urls"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=ResponseAdminSiteEdit}
// @Router /admin/site-edit [post]
func AdminSiteEdit(app *core.App, router fiber.Router) {
router.Post("/site-edit", func(c *fiber.Ctx) error {
var p ParamsAdminSiteEdit
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
site := app.Dao().FindSiteByID(p.ID)
if site.IsEmpty() {
return common.RespError(c, i18n.T("{{name}} not found", Map{"name": i18n.T("Site")}))
}
// 站点操作权限检查
if !common.IsAdminHasSiteAccess(app, c, site.Name) {
return common.RespError(c, i18n.T("Access denied"))
}
if strings.TrimSpace(p.Name) == "" {
return common.RespError(c, i18n.T("{{name}} cannot be empty", Map{"name": "name"}))
}
// 重命名合法性检测
modifyName := p.Name != site.Name
if modifyName && !app.Dao().FindSite(p.Name).IsEmpty() {
return common.RespError(c, i18n.T("{{name}} already exists", Map{"name": i18n.T("Site")}))
}
// urls 合法性检测
if p.Urls != "" {
for _, url := range utils.SplitAndTrimSpace(p.Urls, ",") {
if !utils.ValidateURL(url) {
return common.RespError(c, i18n.T("Contains invalid URL"))
}
}
}
// 预先删除缓存,防止修改主键原有 site_name 占用问题
app.Dao().CacheAction(func(cache *dao.DaoCache) {
cache.SiteCacheDel(&site)
})
// 同步变更 site_name
if modifyName {
var comments []entity.Comment
var pages []entity.Page
app.Dao().DB().Where("site_name = ?", site.Name).Find(&comments)
app.Dao().DB().Where("site_name = ?", site.Name).Find(&pages)
for _, comment := range comments {
comment.SiteName = p.Name
app.Dao().UpdateComment(&comment)
}
for _, page := range pages {
page.SiteName = p.Name
app.Dao().UpdatePage(&page)
}
}
// 修改 site
site.Name = p.Name
site.Urls = p.Urls
err := app.Dao().UpdateSite(&site)
if err != nil {
return common.RespError(c, i18n.T("{{name}} save failed", Map{"name": i18n.T("Site")}))
}
return common.RespData(c, ResponseAdminSiteEdit{
Site: app.Dao().CookSite(&site),
})
})
}
-50
View File
@@ -1,50 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminSiteGet struct {
}
type ResponseAdminSiteGet struct {
Sites []entity.CookedSite `json:"sites"`
}
// @Summary Site List
// @Description Get a list of sites by some conditions
// @Tags Site
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=ResponseAdminSiteGet}
// @Router /admin/site-get [post]
func AdminSiteGet(app *core.App, router fiber.Router) {
router.Post("/site-get", func(c *fiber.Ctx) error {
var p ParamsAdminSiteGet
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
allSites := app.Dao().FindAllSitesCooked()
sites := allSites
// 非超级管理员仅显示分配的站点
if !common.GetIsSuperAdmin(app, c) {
sites = []entity.CookedSite{}
user := common.GetUserByReq(app, c)
userCooked := app.Dao().CookUser(&user)
for _, s := range allSites {
if utils.ContainsStr(userCooked.SiteNames, s.Name) {
sites = append(sites, s)
}
}
}
return common.RespData(c, ResponseAdminSiteGet{
Sites: sites,
})
})
}
-171
View File
@@ -1,171 +0,0 @@
package handler
import (
"bytes"
"encoding/json"
"io"
"os"
"github.com/ArtalkJS/Artalk/internal/artransfer"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
func AdminTransfer(app *core.App, router fiber.Router) {
router.Post("/import", adminImport(app))
router.Post("/import-upload", adminImportUpload(app))
router.Post("/export", adminExport(app))
}
type ParamsAdminImport struct {
Payload string `form:"payload"`
}
// @Summary Transfer Import
// @Description Import data to Artalk
// @Tags Transfer
// @Param payload formData string false "the transfer importer payload"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/import [post]
func adminImport(app *core.App) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
var p ParamsAdminImport
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
var payloadMapRaw map[string]interface{}
err := json.Unmarshal([]byte(p.Payload), &payloadMapRaw)
if err != nil {
return common.RespError(c, "Payload parsing error", common.Map{
"error": err,
})
}
payloadMap := map[string]string{}
for k, v := range payloadMapRaw {
payloadMap[k] = utils.ToString(v) // convert all value to string
}
payloadArr := []string{}
for k, v := range payloadMap {
payloadArr = append(payloadArr, k+":"+v)
}
if !common.GetIsSuperAdmin(app, c) {
user := common.GetUserByReq(app, c)
if sitName, isExist := payloadMap["t_name"]; isExist {
if !utils.ContainsStr(app.Dao().CookUser(&user).SiteNames, sitName) {
return common.RespError(c, "Destination site name of prohibited import")
}
} else {
return common.RespError(c, "Please fill in the target site name")
}
}
// TODO bcz 懒,先整这个缓冲输出,以后改成高级点的
r := c.Response()
r.Header.Add(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
r.SetStatusCode(fiber.StatusOK)
buf := bytes.NewBufferString("")
r.SetBodyStream(buf, -1)
r.ImmediateHeaderFlush = true
buf.Write([]byte(
`<style>* { font-family: Menlo, Consolas, Monaco, monospace;word-wrap: break-word;white-space: pre-wrap;font-size: 13px; }</style>
<script>function scroll() { if (!!document.body) { document.body.scrollTo(0, 999999999999); } }</script>`))
artransfer.HttpOutput = func(continueRun bool, text string) {
buf.Write([]byte(text))
buf.Write([]byte("<script>scroll();</script>"))
}
params := artransfer.ArrToImportParams(payloadArr)
params.Assumeyes = true
artransfer.RunImportArtrans(app.Dao(), params)
return nil
}
}
// @Summary Transfer Import Upload
// @Description Upload a file to prepare to import
// @Tags Transfer
// @Param file formData file true "upload file in preparation for import task"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult
// @Router /admin/import-upload [post]
func adminImportUpload(app *core.App) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
// 获取 Form
file, err := c.FormFile("file")
if err != nil {
log.Error(err)
return common.RespError(c, "File read failed")
}
// 打开文件
src, err := file.Open()
if err != nil {
log.Error(err)
return common.RespError(c, "File open failed")
}
defer src.Close()
// 读取文件
buf, err := io.ReadAll(src)
if err != nil {
log.Error(err)
return common.RespError(c, "File read failed")
}
tmpFile, err := os.CreateTemp("", "artalk-import-file-")
if err != nil {
log.Error(err)
return common.RespError(c, "tmp file creation failed")
}
tmpFile.Write(buf)
return common.RespData(c, common.Map{
"filename": tmpFile.Name(),
})
}
}
// @Summary Transfer Export
// @Description Export data from Artalk
// @Tags Transfer
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=object{data=string}}
// @Router /admin/export [post]
func adminExport(app *core.App) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
var siteNameScope []string
if !common.GetIsSuperAdmin(app, c) {
// 仅导出限定范围内的站点
u := common.GetUserByReq(app, c)
siteNameScope = app.Dao().CookUser(&u).SiteNames
}
jsonStr, err := artransfer.RunExportArtrans(app.Dao(), &artransfer.ExportParams{
SiteNameScope: siteNameScope,
})
if err != nil {
common.RespError(c, i18n.T("Export error"), common.Map{
"err": err,
})
}
return common.RespData(c, common.Map{
"data": jsonStr,
})
}
}
-93
View File
@@ -1,93 +0,0 @@
package handler
import (
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/ArtalkJS/Artalk/server/common"
"github.com/gofiber/fiber/v2"
)
type ParamsAdminUserAdd struct {
Name string `form:"name" validate:"required"`
Email string `form:"email" validate:"required"`
Password string `form:"password"`
Link string `form:"link"`
IsAdmin bool `form:"is_admin" validate:"required"`
SiteNames string `form:"site_names"`
ReceiveEmail bool `form:"receive_email" validate:"required"`
BadgeName string `form:"badge_name"`
BadgeColor string `form:"badge_color"`
}
type ResponseAdminUserAdd struct {
User entity.CookedUserForAdmin `json:"user"`
}
// @Summary User Add
// @Description Create a new user
// @Tags User
// @Param name formData string true "the user name"
// @Param email formData string true "the user email"
// @Param password formData string false "the user password"
// @Param link formData string false "the user link"
// @Param is_admin formData bool true "the user is an admin"
// @Param site_names formData string false "the site names associated with the user"
// @Param receive_email formData bool true "the user receive email"
// @Param badge_name formData string false "the user badge name"
// @Param badge_color formData string false "the user badge color (hex format)"
// @Security ApiKeyAuth
// @Success 200 {object} common.JSONResult{data=ResponseAdminUserAdd}
// @Router /admin/user-add [post]
func AdminUserAdd(app *core.App, router fiber.Router) {
router.Post("/user-add", func(c *fiber.Ctx) error {
if !common.GetIsSuperAdmin(app, c) {
return common.RespError(c, i18n.T("Access denied"))
}
var p ParamsAdminUserAdd
if isOK, resp := common.ParamsDecode(c, &p); !isOK {
return resp
}
if !app.Dao().FindUser(p.Name, p.Email).IsEmpty() {
return common.RespError(c, i18n.T("{{name}} already exists", Map{"name": i18n.T("User")}))
}
if !utils.ValidateEmail(p.Email) {
return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Email")}))
}
if p.Link != "" && !utils.ValidateURL(p.Link) {
return common.RespError(c, i18n.T("Invalid {{name}}", Map{"name": i18n.T("Link")}))
}
user := entity.User{}
user.Name = p.Name
user.Email = p.Email
user.Link = p.Link
user.IsAdmin = p.IsAdmin
user.SiteNames = p.SiteNames
user.ReceiveEmail = p.ReceiveEmail
user.BadgeName = p.BadgeName
user.BadgeColor = p.BadgeColor
if p.Password != "" {
err := user.SetPasswordEncrypt(p.Password)
if err != nil {
log.Errorln(err)
return common.RespError(c, i18n.T("Password update failed"))
}
}
err := app.Dao().CreateUser(&user)
if err != nil {
return common.RespError(c, i18n.T("{{name}} creation failed", Map{"name": i18n.T("User")}))
}
return common.RespData(c, ResponseAdminUserAdd{
User: app.Dao().UserToCookedForAdmin(&user),
})
})
}
Loaded 100 of 318 files, more files were not shown because too many files have changed in this diff. Show more