Compare commits

...
100 Commits
Author SHA1 Message Date
github-actions[bot] a0892fc4c0 chore: release v2.6.2 2023-09-12 09:43:50 +00:00
qwqcode c1e365fdfd chore: relicense from LGPLv3 to MIT 2023-09-12 17:02:41 +08:00
qwqcode e6e6d2aa54 perf(style): modify z-index less than 5 (#578) 2023-09-12 16:41:11 +08:00
qwqcode ac70cd4c9d fix(cmd): remove app instance creation when mini boot (#577) 2023-09-12 16:35:02 +08:00
qwqcode 600030b528 chore(script): move dev config from local to data folder 2023-09-12 16:09:37 +08:00
github-actions[bot] 4e1537b4f0 chore: release v2.6.1 2023-09-11 08:37:27 +00:00
Moraxyc 8a8aac945d docs(extras): add deploy guide for railway.app (#567) 2023-09-11 16:35:55 +08:00
Moraxycandqwqcode 89be748dee docs(config): add info about env variables (#566)
* doc(config): add info about env variables

* modify

* modify

---------

Co-authored-by: qwqcode <qwqcode@gmail.com>
2023-09-11 16:34:26 +08:00
qwqcode 6c35fe9ded fix(captcha): image captcha base64 response issue (#575) 2023-09-11 15:17:11 +08:00
qwqcode 9ea6e8d981 build: fix build-app workflow by disable cache 2023-09-01 21:59:57 +08:00
github-actions[bot] 2c89c2d633 chore: release v2.6.0 2023-09-01 13:15:41 +00:00
qwqcode 5eddd9ff05 feat(config): support load env variables as config (#564) 2023-09-01 19:34:57 +08:00
qwqcode 9fdfb2a043 fix(ui): fix plug cannot disable issue (#563) 2023-09-01 17:56:45 +08:00
qwqcode 2bdb806aaf refactor(conf): better config.NewFromFile with error handle 2023-09-01 17:02:05 +08:00
qwqcode a1b3f1a1f7 fix(cache): child comment ids store after comment updated 2023-09-01 17:02:05 +08:00
qwqcode dc333e5a9a refactor(core): better AppService func with error handle 2023-09-01 17:02:04 +08:00
qwqcode f0ecff9199 fix(config): fix ip region patch 2023-09-01 17:02:04 +08:00
qwqcode e3ccc655c5 fix(limiter): limiter log ignore options method req 2023-09-01 17:02:04 +08:00
qwqcode 04a96d4ab8 chore(build): upgrade pnpm and node 2023-09-01 17:02:03 +08:00
qwqcode 71e0fcec75 fix(limiter): fix limiter always mode 2023-09-01 17:02:03 +08:00
qwqcode 963d3cf3d9 fix(cmd): fix parse root cmd global flags 2023-09-01 17:02:03 +08:00
qwqcode 2669b0efe8 perf(cmd): support command mini boot mode 2023-09-01 17:02:03 +08:00
qwqcode a8b4eb6047 test(utils): add tests for utils 2023-09-01 17:02:02 +08:00
qwqcode cc7b2f574c test(ip_region): add tests for ip_region 2023-09-01 17:02:02 +08:00
qwqcode 150de6d7c0 test(limiter): add tests for limiter 2023-09-01 17:02:02 +08:00
qwqcode 1da3f6420e test(artransfer): add tests for artransfer 2023-09-01 17:02:02 +08:00
qwqcode 919dfa43d0 test(db): add tests for db 2023-09-01 17:02:01 +08:00
qwqcode b6805ce948 fix(db): fix dsn strings for mysql connections (#541) 2023-09-01 17:02:01 +08:00
qwqcode 137610aa82 test(i18n): add tests for i18n 2023-09-01 17:02:01 +08:00
qwqcode 0cbb5b2dd1 test(core): add tests for core 2023-09-01 17:02:00 +08:00
qwqcode 9e59fefc8a test(cache): add tests for cache and dao cache adaptor 2023-09-01 17:02:00 +08:00
qwqcode 6ead92db54 refactor(cache): cache code tidy-up 2023-09-01 17:02:00 +08:00
qwqcode 388b5b8b24 refactor(cache): refactor cache pkg 2023-09-01 17:02:00 +08:00
qwqcode e15e22ecda chore: move .chglog as a subfolder to .github 2023-09-01 17:01:59 +08:00
qwqcode 8336bb0345 chore: fix ci testing 2023-09-01 17:01:59 +08:00
qwqcode fc6c6286db refactor(dao_cache): add dao_cache pkg 2023-09-01 17:01:59 +08:00
qwqcode 3af4482db6 refactor(cache): refactor cache pkg 2023-09-01 17:01:59 +08:00
qwqcode c9f8e9b824 chore: modify pkged type to fs.FS 2023-09-01 17:01:58 +08:00
qwqcode d9c2813e50 chore: improve the naming of middleware code 2023-09-01 17:01:58 +08:00
qwqcode 84d6f9728e chore: fix test app 2023-09-01 17:01:58 +08:00
qwqcode d1b730113b refactor(cors): refactor cors middleware 2023-09-01 17:01:57 +08:00
qwqcode 731be57e74 refactor(main): refactor main.go 2023-09-01 17:01:57 +08:00
qwqcode 9f7fb9dad2 refactor(server/middleware): refactor server/middleware pkg 2023-09-01 17:01:57 +08:00
qwqcode 9f81879713 refactor(server/handler): refactor server/handler pkg 2023-09-01 17:01:57 +08:00
qwqcode ff8527f007 refactor(server/common): refactor server/common pkg 2023-09-01 17:01:56 +08:00
qwqcode 2e07d2a2ca refactor(limiter): add limiter pkg 2023-09-01 17:01:56 +08:00
qwqcode 8f5a84969b refactor(ip_region): refactor ip_region pkg 2023-09-01 17:01:56 +08:00
qwqcode 1e76a4d99f refactor(email): refactor email pkg 2023-09-01 17:01:55 +08:00
qwqcode 840621a6b1 refactor(cloud): refactor cloud pkg 2023-09-01 17:01:55 +08:00
qwqcode 24485f9615 refactor(captcha): refactor captcha pkg 2023-09-01 17:01:55 +08:00
qwqcode d4f0ae8ad7 feat(simple_cache): a simple thread-safe cache with sync.Map 2023-09-01 17:01:55 +08:00
qwqcode a06928378a refactor(cache): refactor cache pkg 2023-09-01 17:01:54 +08:00
qwqcode bf93ca78c8 refactor(artransfer): refactor artransfer pkg 2023-09-01 17:01:54 +08:00
qwqcode 75180877b1 refactor(anti_spam): refactor anti_spam pkg 2023-09-01 17:01:54 +08:00
qwqcode fb4aaf7ecb refactor(cmd): refactor cmd pkg 2023-09-01 17:01:53 +08:00
qwqcode d58355209a refactor(notify): add notify_pusher pkg 2023-09-01 17:01:53 +08:00
qwqcode 8488b7e18b refactor(i18n): refactor i18n pkg 2023-09-01 17:01:53 +08:00
qwqcode e7aced94dd feat(hook): add hook pkg 2023-09-01 17:01:53 +08:00
qwqcode 3dd93c33b3 feat(utils): add common random utils 2023-09-01 17:01:52 +08:00
qwqcode bd613a3304 refactor(config): refactor config pkg 2023-09-01 17:01:52 +08:00
qwqcode 7028dcd905 refactor(log): create common log pkg 2023-09-01 17:01:52 +08:00
qwqcode 9ef063cf98 refactor(db): refactor db pkg 2023-09-01 17:01:52 +08:00
qwqcode bf13e1cdba refactor(test): create common test pkg 2023-09-01 17:01:51 +08:00
qwqcode 79f5f53886 refactor(dao): refactor dao pkg 2023-09-01 17:01:51 +08:00
qwqcode 4c951d7e44 refactor(core): refactor core pkg 2023-09-01 17:01:51 +08:00
qwqcode b034e514a7 refactor(cache): remove cached fields in struct to save memory (#555) 2023-08-23 18:06:03 +08:00
qwqcode b88a26dcae fix(log): add missing log for web server listen func (#556) 2023-08-23 10:48:21 +08:00
qwqcode ee30ba4fc8 chore(CI): increase dependabot open-pull-requests-limit 2023-08-22 17:52:17 +08:00
qwqcode a02b89ba3e chore(CI): add dependabot updates for docs 2023-08-22 17:48:17 +08:00
dependabot[bot] 2ca3ab8274 chore(deps/ui): bump the prod-deps group in /ui with 1 update (#552)
Bumps the prod-deps group in /ui with 1 update: [katex](https://github.com/KaTeX/KaTeX).

- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.4...v0.16.8)

---
updated-dependencies:
- dependency-name: katex
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 17:45:36 +08:00
dependabot[bot] cbf0210131 chore(deps/ui): bump the dev-deps group in /ui with 5 updates (#553)
Bumps the dev-deps group in /ui with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.5.1` | `20.5.2` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `6.4.0` | `6.4.1` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `6.4.0` | `6.4.1` |
| [rollup](https://github.com/rollup/rollup) | `3.28.0` | `3.28.1` |
| [@vitejs/plugin-vue](https://github.com/vitejs/vite-plugin-vue/tree/HEAD/packages/plugin-vue) | `4.3.1` | `4.3.3` |


Updates `@types/node` from 20.5.1 to 20.5.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@typescript-eslint/eslint-plugin` from 6.4.0 to 6.4.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.4.1/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 6.4.0 to 6.4.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.4.1/packages/parser)

Updates `rollup` from 3.28.0 to 3.28.1
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.28.0...v3.28.1)

Updates `@vitejs/plugin-vue` from 4.3.1 to 4.3.3
- [Release notes](https://github.com/vitejs/vite-plugin-vue/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-vue/blob/main/packages/plugin-vue/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-vue/commits/plugin-vue@4.3.3/packages/plugin-vue)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
- dependency-name: rollup
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
- dependency-name: "@vitejs/plugin-vue"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 17:45:18 +08:00
qwqcode d8d9420616 chore(CI): dependabot use groups 2023-08-22 17:40:39 +08:00
qwqcode 891f792fe4 chore(CI): modify dependabot.yml prefix config 2023-08-22 17:07:46 +08:00
qwqcode d538e47917 chore(CI): fix dependabot.yml 2023-08-22 16:50:14 +08:00
qwqcode 6c7ce98522 refactor(CI): replace renovate with github dependabot (#546) 2023-08-22 16:49:03 +08:00
qwqcode 1e839548a4 refactor(CI): replace using docker/metadata-action for the docker push (#545) 2023-08-22 16:18:20 +08:00
qwqcode 4ea45ae375 chore(ci): upgrade deps in ci 2023-08-22 12:22:50 +08:00
qwqcode 91f89c6b7d chore(deps): update deps in docs 2023-08-22 12:11:20 +08:00
qwqcode 0ac70a5a43 chore(deps): upgrade frontend deps 2023-08-22 12:00:09 +08:00
qwqcode 9ac354cb23 chore(conf): fix default gravatar mirror url for english env in conf 2023-08-21 12:16:13 +08:00
qwqcode 40e6fcd966 feat(go): upgrade golang to v1.21.0 and some deps (#543)
* Upgrade to Golang v1.21.0 and alpine v3.18
2023-08-20 20:11:10 +08:00
WayJam So 7e476ded1c docs(extras): add deploy guide for fly.io (#520) 2023-06-05 19:08:07 +08:00
github-actions[bot] a885c0a179 chore: release v2.5.5 2023-05-11 12:10:33 +00:00
qwqcode 08c0b297e5 feat(gravatar): add gravatar.params config option (#508)
* feat(gravatar): add `gravatar.params` config option

BREAKING CHANGE: The `gravatar.default` config option has been removed. Please use `gravatar.params` instead. The default value for `gravatar.params` is now `"d=mp&s=240"`.

Signed-off-by: qwqcode <qwqcode@gmail.com>

* add docs and fix sidebar

Signed-off-by: qwqcode <qwqcode@gmail.com>

---------

Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-05-10 13:42:04 +08:00
qwqcode 01ed04c791 fix(editor): prevent editor reset on comment failure (#507)
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-05-10 11:40:09 +08:00
qwqcode 0135a83b49 chore(deps): upgrade frontend deps
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-05-10 11:22:12 +08:00
qwqcode b9c177bb54 feat(go): upgrade golang to v1.20.4 and some deps (#506) 2023-05-09 11:06:03 +08:00
qwqcode 00ce23bc99 chore(deps): update some deps in docs 2023-05-09 10:56:24 +08:00
qwqcode 4853b6cc5d chore(build): bump pnpm to v8.4.0 2023-05-09 10:45:42 +08:00
qwqcode 79dea425ea feat(build): add embedded fe lite and i18n scripts (#493) (#505) 2023-05-09 10:41:28 +08:00
qwqcode c20faed4e3 fix(api): comment IP data loss after editing (#504)
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-05-07 18:55:15 +08:00
qwqcode e9acd28126 chore: update renovate.json
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-04-09 20:51:54 +08:00
github-actions[bot] b71b22f5a7 chore: release v2.5.4 2023-04-09 12:34:32 +00:00
qwqcode bc7d0e09b8 chore(deps): upgrade frontend deps
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-04-09 20:14:43 +08:00
qwqcode f682ade2c9 docs: update swagger docs and some deps
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-04-09 20:07:18 +08:00
qwqcode 2b990ff433 feat(go): upgrade golang to v1.20.3 and some deps (#485)
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-04-09 20:06:00 +08:00
qwqcode 6d21c2b074 fix(CI): upgrade and pin node.js and pnpm version (#484)
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-04-09 19:46:52 +08:00
github-actions[bot] 3840720620 chore: release v2.5.3 2023-04-06 13:08:21 +00:00
qwqcode 7dd8668b6e perf(ip_region): add db file existence check and error message (#482)
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-04-06 21:05:37 +08:00
qwqcode ac4b8b0b80 fix(api/comment_add): SIGSEGV crash in comment_add (#478) (#481)
Signed-off-by: qwqcode <qwqcode@gmail.com>
2023-04-06 20:46:27 +08:00
271 changed files with 11742 additions and 7861 deletions

No files matched your search

+5 -5
View File
@@ -9,10 +9,10 @@ executors:
- image: cimg/base:stable
node:
docker:
- image: 'cimg/node:16.15.1'
- image: 'cimg/node:18.17.1'
go:
docker:
- image: cimg/go:1.20
- image: cimg/go:1.21.0
parameters:
trigger-app:
@@ -40,12 +40,12 @@ jobs:
go version
- restore_cache:
keys:
- go-mod-1.20-{{ checksum "go.sum" }}
- go-mod-1.21.0-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: go mod download
- save_cache:
key: go-mod-1.20-{{ checksum "go.sum" }}
key: go-mod-1.21.0-{{ checksum "go.sum" }}
paths:
- "~/go/pkg/mod"
- run:
@@ -67,7 +67,7 @@ jobs:
command: node -v
- run:
name: Install pnpm
command: npm i -g pnpm@7.25.0
command: npm i -g pnpm@8.7.0
- run:
name: Install Dependencies
command: pnpm --dir ./ui install --frozen-lockfile
+1 -1
View File
@@ -8,7 +8,7 @@ setup: true
# the path-filtering orb is required to continue a pipeline based on
# https://circleci.com/developer/orbs/orb/circleci/path-filtering
orbs:
path-filtering: circleci/path-filtering@0.0.1
path-filtering: circleci/path-filtering@1.0.0
workflows:
setup:
@@ -18,4 +18,4 @@
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
{{ end -}}
File renamed without changes.
+67
View File
@@ -0,0 +1,67 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: monthly
commit-message:
prefix: "chore(deps/go): "
groups:
deps:
patterns:
- '*'
open-pull-requests-limit: 15
- package-ecosystem: npm
directory: /ui
schedule:
interval: monthly
commit-message:
prefix: "chore(deps/ui): "
groups:
prod-deps:
dependency-type: production
dev-deps:
dependency-type: development
open-pull-requests-limit: 15
- package-ecosystem: npm
directory: /docs
schedule:
interval: monthly
commit-message:
prefix: "chore(deps/docs): "
groups:
deps:
patterns:
- '*'
open-pull-requests-limit: 15
- package-ecosystem: docker
directory: /
schedule:
interval: monthly
commit-message:
prefix: "chore(deps/docker): "
groups:
deps:
patterns:
- '*'
open-pull-requests-limit: 15
- package-ecosystem: github-actions
directory: /.github/workflows
schedule:
interval: monthly
commit-message:
prefix: "chore(deps/ci): "
groups:
deps:
patterns:
- '*'
open-pull-requests-limit: 15
-12
View File
@@ -1,12 +0,0 @@
{
"extends": ["config:base", "schedule:weekly"],
"automerge": false,
"major": {
"automerge": false
},
"branchPrefix": "chore/",
"labels": ["dependencies"],
"gomodTidy": true,
"rangeStrategy": "bump",
"reviewers": []
}
+16 -14
View File
@@ -6,7 +6,7 @@ on:
types: [artalk-release]
env:
GO_VERSION: '1.20.2'
GO_VERSION: '1.21.0'
PKG_NAME: 'github.com/ArtalkJS/Artalk'
DOCKER_IMG: ghcr.io/goreleaser/goreleaser-cross
CACHE_DIR: /tmp/cache/docker-image
@@ -30,23 +30,25 @@ jobs:
echo "VERSION=${VERSION}" >> $GITHUB_ENV
- name: Docker Image Cache
id: docker-cache
uses: actions/cache@v3
with:
path: ${{ env.CACHE_DIR }}
key: docker-image-go-cross-${{ env.GO_VERSION }}
# disable cache because the err `no space left on device`
# - name: Docker Image Cache
# id: docker-cache
# uses: actions/cache@v3
# with:
# path: ${{ env.CACHE_DIR }}
# key: docker-image-go-cross-${{ env.GO_VERSION }}
- name: Pull Docker Image
if: steps.docker-cache.outputs.cache-hit != 'true'
# if: steps.docker-cache.outputs.cache-hit != 'true'
run: |
docker pull "${DOCKER_IMG}:v${GO_VERSION}"
mkdir -p "${CACHE_DIR}"
docker save -o "${CACHE_DIR}/go-cross.tar" "${DOCKER_IMG}:v${GO_VERSION}"
- name: Restore Docker Image from Cache
if: steps.docker-cache.outputs.cache-hit == 'true'
run: docker load -i ${CACHE_DIR}/go-cross.tar
# mkdir -p "${CACHE_DIR}"
# docker save -o "${CACHE_DIR}/go-cross.tar" "${DOCKER_IMG}:v${GO_VERSION}"
# - name: Restore Docker Image from Cache
# if: steps.docker-cache.outputs.cache-hit == 'true'
# run: docker load -i ${CACHE_DIR}/go-cross.tar
- name: Setup git-chglog
run: |
@@ -62,7 +64,7 @@ jobs:
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" > local/.release-env
# changelog
git-chglog ${VERSION} > local/release-notes.md
git-chglog --config .github/chglog/config.yml ${VERSION} > local/release-notes.md
# copy config file
cp conf/artalk.example.yml artalk.yml
+3 -9
View File
@@ -62,17 +62,12 @@ jobs:
restore-keys: |
${{ runner.os }}-buildx-
# TODO: https://github.com/docker/metadata-action/pull/248
- name: Get Docker Tags and Labels
run: |
TAGS="${DOCKER_IMG}:latest,${DOCKER_IMG}:${VERSION#v},${DOCKER_IMG}:${VERSION_SHORT},${DOCKER_IMG}:sha-${COMMIT_HASH}"
echo "DOCKER_TAGS=${TAGS}" >> $GITHUB_ENV
# https://github.com/docker/metadata-action
- name: Gen docker meta
id: meta
uses: docker/metadata-action@v4
with:
context: git
images: |
${{ env.DOCKER_IMG }}
tags: |
@@ -85,15 +80,14 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and Push
id: docker_build
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
builder: ${{ steps.buildx.outputs.name }}
context: ./
file: ./Dockerfile
platforms: 'linux/amd64,linux/arm64'
push: true
# tags: ${{ steps.meta.outputs.tags }}
tags: ${{ env.DOCKER_TAGS }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
+2 -2
View File
@@ -25,12 +25,12 @@ jobs:
- name: Use pnpm
uses: pnpm/action-setup@v2
with:
version: 7
version: 8
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x
registry-url: https://registry.npmjs.org/
cache: 'pnpm'
cache-dependency-path: './ui/pnpm-lock.yaml'
+2 -2
View File
@@ -35,12 +35,12 @@ jobs:
- name: Use pnpm
uses: pnpm/action-setup@v2
with:
version: 7
version: 8
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x
registry-url: https://registry.npmjs.org/
cache: 'pnpm'
+2 -2
View File
@@ -34,7 +34,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: 18.x
registry-url: https://registry.npmjs.org/
- name: Handle Version Number
@@ -66,7 +66,7 @@ jobs:
run: |
if [ ! $(git tag -l "${VERSION}") ]; then
# prepend changelog
changelog=$(git-chglog --next-tag "${VERSION}" "${VERSION}")
changelog=$(git-chglog --config .github/chglog/config.yml --next-tag "${VERSION}" "${VERSION}")
echo -e "${changelog}\n" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md
# modify the version of `package.json`
+1
View File
@@ -48,6 +48,7 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
__debug*
# frontend
node_modules
+2 -2
View File
@@ -8,7 +8,7 @@
"mode": "exec",
"program": "${workspaceFolder}/bin/artalk",
"preLaunchTask": "Build for debugging",
"args": ["server", "-c", "${workspaceFolder}/local/local.yml"]
"args": ["server", "-c", "${workspaceFolder}/data/artalk.yml"]
}
],
}
}
-75
View File
@@ -1,75 +0,0 @@
<p align="center">
<img src="./docs/artalk.png" alt="Artalk" width="100%">
</p>
# Artalk
[![CircleCI](https://circleci.com/gh/ArtalkJS/Artalk/tree/master.svg?style=svg)](https://circleci.com/gh/ArtalkJS/Artalk/tree/master)
[![CI to Docker Hub](https://github.com/ArtalkJS/Artalk/actions/workflows/dockerhub.yml/badge.svg)](https://github.com/ArtalkJS/Artalk/actions/workflows/dockerhub.yml) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_shield)
[![Docker Pulls](https://img.shields.io/docker/pulls/artalk/artalk-go?style=flat-square)](https://hub.docker.com/r/artalk/artalk-go)
> Artalk: Golang backend of Artalk.
前往:[“**官方文档 · 后端部分**”](https://artalk.js.org/guide/backend/config.html)
---
- 高效快速
- 异步执行
- 跨平台兼容
- 轻量级部署
## Supports
- 运行环境:支持 Linux, Windows, Darwin (x64 + ARM)
- 数据存储:支持 SQLite, MySQL, PostgreSQL, SQL Server
- 邮件发送:支持 SMTP, 阿里云邮件, 调用 sendmail 发送邮件
- 高效缓存:支持 Redis, Memcache, In-Memory (BigCache)
## Build
> 部署可以使用官方提供的 Docker 镜像,见:[后端部署文档](https://artalk.js.org/guide/backend/install.html)
### 编译二进制文件
```sh
$ make all
```
编译后二进制文件将输出到 `bin/` 目录下
### Docker Compose 编译运行
```sh
# 克隆项目
$ git clone https://github.com/ArtalkJS/Artalk
$ cd Artalk
# 构建镜像
$ docker compose build
# 运行
$ docker compose up -d
```
### Docker 镜像构建
```sh
# 克隆项目
$ git clone https://github.com/ArtalkJS/Artalk
$ cd Artalk
# 构建镜像
$ make docker-build
# 发布镜像
$ make docker-push
```
## TODOs
Reference to https://github.com/ArtalkJS/Artalk#todos
## License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_large)
+132
View File
@@ -1,4 +1,136 @@
## [v2.6.2](https://github.com/ArtalkJS/Artalk/compare/v2.6.1...v2.6.2) (2023-09-12)
### Bug Fixes
* **cmd:** remove app instance creation when mini boot ([#577](https://github.com/ArtalkJS/Artalk/issues/577))
### Performance Improvements
* **style:** modify `z-index` less than 5 ([#578](https://github.com/ArtalkJS/Artalk/issues/578))
## [v2.6.1](https://github.com/ArtalkJS/Artalk/compare/v2.6.0...v2.6.1) (2023-09-11)
### Bug Fixes
* **captcha:** image captcha base64 response issue ([#575](https://github.com/ArtalkJS/Artalk/issues/575))
### Documentation
* **config:** add info about env variables ([#566](https://github.com/ArtalkJS/Artalk/issues/566))
* **extras:** add deploy guide for railway.app ([#567](https://github.com/ArtalkJS/Artalk/issues/567))
## [v2.6.0](https://github.com/ArtalkJS/Artalk/compare/v2.5.5...v2.6.0) (2023-09-01)
### Features
* **config:** support load env variables as config ([#564](https://github.com/ArtalkJS/Artalk/issues/564))
* **go:** upgrade golang to v1.21.0 and some deps ([#543](https://github.com/ArtalkJS/Artalk/issues/543))
* **hook:** add hook pkg
* **simple_cache:** a simple thread-safe cache with sync.Map
* **utils:** add common random utils
### Bug Fixes
* **cache:** child comment ids store after comment updated
* **cmd:** fix parse root cmd global flags
* **config:** fix ip region patch
* **db:** fix dsn strings for mysql connections ([#541](https://github.com/ArtalkJS/Artalk/issues/541))
* **limiter:** limiter log ignore options method req
* **limiter:** fix limiter always mode
* **log:** add missing log for web server listen func ([#556](https://github.com/ArtalkJS/Artalk/issues/556))
* **ui:** fix plug cannot disable issue ([#563](https://github.com/ArtalkJS/Artalk/issues/563))
### Performance Improvements
* **cmd:** support command mini boot mode
### Code Refactoring
* **CI:** replace using `docker/metadata-action` for the docker push ([#545](https://github.com/ArtalkJS/Artalk/issues/545))
* **CI:** replace renovate with github dependabot ([#546](https://github.com/ArtalkJS/Artalk/issues/546))
* **anti_spam:** refactor anti_spam pkg
* **artransfer:** refactor artransfer pkg
* **cache:** remove cached fields in struct to save memory ([#555](https://github.com/ArtalkJS/Artalk/issues/555))
* **cache:** cache code tidy-up
* **cache:** refactor cache pkg
* **cache:** refactor cache pkg
* **cache:** refactor cache pkg
* **captcha:** refactor captcha pkg
* **cloud:** refactor cloud pkg
* **cmd:** refactor cmd pkg
* **conf:** better config.NewFromFile with error handle
* **config:** refactor config pkg
* **core:** refactor core pkg
* **core:** better AppService func with error handle
* **cors:** refactor cors middleware
* **dao:** refactor dao pkg
* **dao_cache:** add dao_cache pkg
* **db:** refactor db pkg
* **email:** refactor email pkg
* **i18n:** refactor i18n pkg
* **ip_region:** refactor ip_region pkg
* **limiter:** add limiter pkg
* **log:** create common log pkg
* **main:** refactor main.go
* **notify:** add notify_pusher pkg
* **server/common:** refactor server/common pkg
* **server/handler:** refactor server/handler pkg
* **server/middleware:** refactor server/middleware pkg
* **test:** create common test pkg
### Documentation
* **extras:** add deploy guide for fly.io ([#520](https://github.com/ArtalkJS/Artalk/issues/520))
## [v2.5.5](https://github.com/ArtalkJS/Artalk/compare/v2.5.4...v2.5.5) (2023-05-10)
### Features
* **build:** add embedded fe lite and i18n scripts ([#493](https://github.com/ArtalkJS/Artalk/issues/493)) ([#505](https://github.com/ArtalkJS/Artalk/issues/505))
* **go:** upgrade golang to v1.20.4 and some deps ([#506](https://github.com/ArtalkJS/Artalk/issues/506))
* **gravatar:** add `gravatar.params` config option ([#508](https://github.com/ArtalkJS/Artalk/issues/508))
### Bug Fixes
* **api:** comment IP data loss after editing ([#504](https://github.com/ArtalkJS/Artalk/issues/504))
* **editor:** prevent editor reset on comment failure ([#507](https://github.com/ArtalkJS/Artalk/issues/507))
### BREAKING CHANGE
The `gravatar.default` config option has been removed. Please use `gravatar.params` instead. The default value for `gravatar.params` is now `"d=mp&s=240"`.
## [v2.5.4](https://github.com/ArtalkJS/Artalk/compare/v2.5.3...v2.5.4) (2023-04-09)
### Features
* **go:** upgrade golang to v1.20.3 and some deps ([#485](https://github.com/ArtalkJS/Artalk/issues/485))
### Bug Fixes
* **CI:** upgrade and pin node.js and pnpm version ([#484](https://github.com/ArtalkJS/Artalk/issues/484))
### Documentation
* update swagger docs and some deps
## [v2.5.3](https://github.com/ArtalkJS/Artalk/compare/v2.5.2...v2.5.3) (2023-04-06)
### Bug Fixes
* **api/comment_add:** SIGSEGV crash in comment_add ([#478](https://github.com/ArtalkJS/Artalk/issues/478)) ([#481](https://github.com/ArtalkJS/Artalk/issues/481))
### Performance Improvements
* **ip_region:** add db file existence check and error message ([#482](https://github.com/ArtalkJS/Artalk/issues/482))
## [v2.5.2](https://github.com/ArtalkJS/Artalk/compare/v2.5.1...v2.5.2) (2023-03-19)
### Bug Fixes
+4 -4
View File
@@ -7,7 +7,7 @@ This guide is for developers who want to contribute to the project.
To develop Artalk, including the frontend and backend, you need to install the following tools:
- [Node.js](https://nodejs.org/en/) (>= 16.0.0)
- [Go](https://golang.org/) (>= 1.20)
- [Go](https://golang.org/) (>= 1.21)
- [Docker](https://www.docker.com/) (>= 20.10.0) (optional)
- [Docker Compose](https://docs.docker.com/compose/) (>= 1.29.0) (optional)
@@ -50,12 +50,12 @@ If you want to run a demo site, you can use the following command:
./scripts/setup-example-site.sh
```
This script sets up a local example site for testing at `local` folder, with Artalk integrated into its theme.
This script sets up a local example site for testing at `data` folder, with Artalk integrated into its theme.
After running this script, run:
```sh
./bin/artalk server -c ./local/local.yml
./bin/artalk server -c ./data/artalk.yml
```
to start the artalk server.
@@ -80,7 +80,7 @@ Artalk is a monorepo project, which means all the source code is in the same rep
- `docs/` - The documentation site source code.
- `i18n/` - The translation files.
- `internal/` - The internal packages.
- `local/` - The local example site. This directory is ignored by git.
- `data/` - The local data. This directory is ignored by git.
- `public/` - The static files. Built frontend files will be copied here.
- `scripts/` - The scripts for development.
- `server/` - The source code for the server.
+3 -3
View File
@@ -1,12 +1,12 @@
### build Artalk
FROM golang:1.20.2-alpine3.17 as builder
FROM golang:1.21.0-alpine3.18 as builder
WORKDIR /source
# install tools
RUN set -ex \
&& apk add --no-cache make git gcc musl-dev nodejs bash npm\
&& npm install -g pnpm@7.25.0
&& npm install -g pnpm@8.7.0
# download go deps
# (cache by separating the downloading of deps)
@@ -23,7 +23,7 @@ RUN set -ex \
&& make build
### build final image
FROM alpine:3.17
FROM alpine:3.18
# we set the timezone `Asia/Shanghai` by default, you can be modified
# by `docker build --build-arg="TZ=Other_Timezone ..."`
+17 -161
View File
@@ -1,165 +1,21 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
The MIT License (MIT)
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Copyright (c) 2018-present, qwqcode and other contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+5 -1
View File
@@ -44,6 +44,10 @@ test:
test-coverage:
$(GOTEST) -cover ./...
test-coverage-html:
$(GOTEST) -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
update-i18n:
go generate ./internal/i18n
@@ -57,5 +61,5 @@ docker-push:
./scripts/docker-build.sh --push
.PHONY: all install build build-frontend build-debug \
dev test test-coverage update-i18n \
dev test test-coverage test-coverage-html update-i18n \
docker-build docker-push;
+1 -1
View File
@@ -131,7 +131,7 @@ Thanks for the help and feedback provided by the community, if you have good sug
## License
[LGPL-3.0](./LICENSE)
[MIT](./LICENSE)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_shield)
+1 -1
View File
@@ -219,7 +219,7 @@ see [CONTRIBUTING.md](./CONTRIBUTING.md)
## License
[LGPL-3.0](./LICENSE)
[MIT](./LICENSE)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_shield)
+44 -49
View File
@@ -2,72 +2,67 @@ package cmd
import (
"bufio"
"errors"
"fmt"
"os"
"strings"
"syscall"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/query"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var adminCmd = &cobra.Command{
Use: "admin",
Short: "Create or edit an administrator account",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
core.LoadCore(cfgFile, workDir)
func NewAdminCommand(app *ArtalkCmd) *cobra.Command {
adminCmd := &cobra.Command{
Use: "admin",
Short: "Create or edit an administrator account",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("--------------------------------")
fmt.Println(" " + i18n.T("Create admin account"))
fmt.Println("--------------------------------")
fmt.Println("--------------------------------")
fmt.Println(" " + i18n.T("Create admin account"))
fmt.Println("--------------------------------")
username, email, password, err := credentials()
if err != nil {
logrus.Fatal(err)
}
findUser := query.FindUser(username, email)
if !findUser.IsEmpty() {
findUser.SetPasswordEncrypt(password)
if err := query.UpdateUser(&findUser); err != nil {
logrus.Fatal(err)
username, email, password, err := credentials()
if err != nil {
log.Fatal(err)
}
logrus.Info(i18n.T("{{name}} already exists", map[string]interface{}{"name": i18n.T("Account")}) +
", " + i18n.T("Password updated"))
return
}
findUser := app.Dao().FindUser(username, email)
if !findUser.IsEmpty() {
findUser.SetPasswordEncrypt(password)
if err := app.Dao().UpdateUser(&findUser); err != nil {
log.Fatal(err)
}
user := entity.User{
Name: username,
Email: email,
IsAdmin: true,
BadgeName: i18n.T("Admin"),
BadgeColor: "#0083FF",
}
user.SetPasswordEncrypt(password)
log.Info(i18n.T("{{name}} already exists", map[string]interface{}{"name": i18n.T("Account")}) +
", " + i18n.T("Password updated"))
return
}
if err := query.CreateUser(&user); err != nil {
logrus.Fatal(err)
}
user := entity.User{
Name: username,
Email: email,
IsAdmin: true,
BadgeName: i18n.T("Admin"),
BadgeColor: "#0083FF",
}
user.SetPasswordEncrypt(password)
fmt.Println("--------------------------------")
fmt.Println(" Name: " + username)
fmt.Println(" Mail: " + email)
fmt.Println("--------------------------------")
},
}
if err := app.Dao().CreateUser(&user); err != nil {
log.Fatal(err)
}
func init() {
rootCmd.AddCommand(adminCmd)
fmt.Println("--------------------------------")
fmt.Println(" Name: " + username)
fmt.Println(" Mail: " + email)
fmt.Println("--------------------------------")
},
}
return adminCmd
}
func credentials() (string, string, string, error) {
@@ -85,7 +80,7 @@ func credentials() (string, string, string, error) {
return "", "", "", err
}
if !utils.ValidateEmail(strings.TrimSpace(email)) {
return "", "", "", errors.New("invalid email format")
return "", "", "", fmt.Errorf("invalid email format")
}
fmt.Print(i18n.T("Enter {{name}}", map[string]interface{}{"name": i18n.T("Password")}) + ": ")
@@ -107,7 +102,7 @@ func credentials() (string, string, string, error) {
rePassword := strings.TrimSpace(string(byteRePassword))
if rePassword != password {
return "", "", "", errors.New("inconsistent password input")
return "", "", "", fmt.Errorf("inconsistent password input")
}
return strings.TrimSpace(username), strings.TrimSpace(email), password, nil
+223
View File
@@ -0,0 +1,223 @@
package cmd
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
var Version = config.Version + `/` + config.CommitHash
var Banner = `
________ ________ _________ ________ ___ ___ __
|\ __ \|\ __ \|\___ ___\\ __ \|\ \ |\ \|\ \
\ \ \|\ \ \ \|\ \|___ \ \_\ \ \|\ \ \ \ \ \ \/ /|_
\ \ __ \ \ _ _\ \ \ \ \ \ __ \ \ \ \ \ ___ \
\ \ \ \ \ \ \\ \| \ \ \ \ \ \ \ \ \ \____\ \ \\ \ \
\ \__\ \__\ \__\\ _\ \ \__\ \ \__\ \__\ \_______\ \__\\ \__\
\|__|\|__|\|__|\|__| \|__| \|__|\|__|\|_______|\|__| \|__|
Artalk (` + Version + `)
-> A Selfhosted Comment System.
-> https://artalk.js.org
`
type ArtalkCmd struct {
*core.App
cfgFile string
workDir string
RootCmd *cobra.Command
}
const BootModeKey = "BootMode"
const (
MODE_FULL_BOOT = "FULL_BOOT"
MODE_MINI_BOOT = "MINI_BOOT"
)
func New() *ArtalkCmd {
cmd := &ArtalkCmd{
RootCmd: &cobra.Command{
Use: "artalk",
Short: "Artalk: Your self-hosted comment system",
Long: Banner,
Version: Version,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(Banner)
fmt.Print("-------------------------------\n\n")
fmt.Println("NOTE: add `-h` flag to show help about any command.")
},
},
}
cmd.RootCmd.SetVersionTemplate("Artalk ({{printf \"%s\" .Version}})\n")
// Parse base flags
cmd.eagerParseFlags()
// Change work directory
if cmd.workDir != "" {
if err := os.Chdir(cmd.workDir); err != nil {
log.Fatal("Working directory change error: ", err)
}
}
return cmd
}
// Parses the global app flags before calling atk.RootCmd.Execute().
// so we can have all flags ready for use on initialization.
func (atk *ArtalkCmd) eagerParseFlags() error {
atk.RootCmd.PersistentFlags().StringVarP(&atk.cfgFile, "config", "c", "", "config file path (defaults are './artalk.yml')")
atk.RootCmd.PersistentFlags().StringVarP(&atk.workDir, "workdir", "w", "", "program working directory (defaults are './')")
return atk.RootCmd.ParseFlags(os.Args[1:])
}
func (atk *ArtalkCmd) addCommand(cmd *cobra.Command) {
originalPreRunFunc := cmd.PreRun
cmd.PreRun = func(cmd *cobra.Command, args []string) {
// ================
// APP Bootstrap
// ================
if cmd.Annotations[BootModeKey] != string(MODE_MINI_BOOT) {
// Load config
config, err := getConfig(atk.cfgFile)
if err != nil {
log.Fatal("Config fail: ", err)
}
// Create new instance
atk.App = core.NewApp(config)
// Bootstrap APP
if err := atk.App.Bootstrap(); err != nil {
panic(err)
}
}
if originalPreRunFunc != nil {
originalPreRunFunc(cmd, args) // extends original pre-run logic
}
}
atk.RootCmd.AddCommand(cmd)
}
func (atk *ArtalkCmd) mountCommands() {
atk.addCommand(NewServeCommand(atk))
atk.addCommand(NewAdminCommand(atk))
atk.addCommand(NewExportCommand(atk))
atk.addCommand(NewImportCommand(atk))
atk.addCommand(NewConfigCommand())
atk.addCommand(NewGenCommand())
atk.addCommand(NewUpgradeCommand())
atk.addCommand(NewVersionCommand())
}
func (atk *ArtalkCmd) Launch() error {
// ===================
// 1. Prepare Commands
// ===================
atk.mountCommands()
done := make(chan bool, 1) // shutdown signal
// listen for interrupt signal to gracefully shutdown the application
go func() {
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, os.Interrupt, syscall.SIGTERM)
<-sigch
done <- true
}()
// ===================
// 2. Command Execute
// ===================
go func() {
if err := atk.RootCmd.Execute(); err != nil {
color.Red(err.Error())
}
done <- true
}()
<-done
// ===================
// 3. App Cleanups
// ===================
if atk.App != nil {
return atk.App.OnTerminate().Trigger(&core.TerminateEvent{
App: atk.App,
})
}
return nil
}
// Create new config instance by specific config filename
func getConfig(cfgFile string) (*config.Config, error) {
// Retrieve config file by default names when specific filename is empty
if cfgFile == "" {
cfgFile = config.RetrieveConfigFile()
}
// Generate new config file when retrieve failed
if cfgFile == "" {
cfgFile = config.CONF_DEFAULT_FILENAMES[0]
core.Gen("config", cfgFile, false)
}
// Create new config instance and return
return config.NewFromFile(cfgFile)
}
// -------------------------------------------------------------------
// Shortcut Functions
// -------------------------------------------------------------------
func flag(cmd *cobra.Command, name string, defaultVal interface{}, usage string) {
f := cmd.PersistentFlags()
switch y := defaultVal.(type) {
case bool:
f.Bool(name, y, usage)
case int:
f.Int(name, y, usage)
case string:
f.String(name, y, usage)
}
}
func flagP(cmd *cobra.Command, name, shorthand string, defaultVal interface{}, usage string) {
f := cmd.PersistentFlags()
switch y := defaultVal.(type) {
case bool:
f.BoolP(name, shorthand, y, usage)
case int:
f.IntP(name, shorthand, y, usage)
case string:
f.StringP(name, shorthand, y, usage)
}
}
func flagV(cmd *cobra.Command, name string, defaultVal interface{}, usage string) {
flag(cmd, name, defaultVal, usage)
}
func flagPV(cmd *cobra.Command, name, shorthand string, defaultVal interface{}, usage string) {
flagP(cmd, name, shorthand, defaultVal, usage)
}
+35
View File
@@ -0,0 +1,35 @@
package cmd
import (
"encoding/json"
"fmt"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/spf13/cobra"
)
func NewConfigCommand() *cobra.Command {
configCmd := &cobra.Command{
Use: "config",
Short: "Output Config Information",
Run: func(cmd *cobra.Command, args []string) {
// Get config filename from cmd flags
filename, _ := cmd.Flags().GetString("config")
// Get new config instance
config, err := getConfig(filename)
if err != nil {
log.Fatal("Config fail: ", err)
}
// Output JSON of config
buf, _ := json.MarshalIndent(config, "", " ")
fmt.Println(string(buf))
},
Annotations: map[string]string{
BootModeKey: MODE_MINI_BOOT,
},
}
return configCmd
}
+47 -50
View File
@@ -8,68 +8,65 @@ import (
"time"
"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/sirupsen/logrus"
"github.com/spf13/cobra"
)
var exportCmd = &cobra.Command{
Use: "export",
Aliases: []string{},
Short: "Artransfer - Export",
Long: "\n# Artransfer - Export\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html",
Run: func(cmd *cobra.Command, args []string) {
core.LoadCore(cfgFile, workDir)
jsonStr, err := artransfer.ExportArtransString()
if err != nil {
logrus.Fatal(err)
}
if len(args) < 1 || args[0] == "" {
// write to stdout
fmt.Println(jsonStr)
} else {
filename := args[0]
// make sure is abs path
filename, err := filepath.Abs(filename)
func NewExportCommand(app *ArtalkCmd) *cobra.Command {
exportCmd := &cobra.Command{
Use: "export",
Aliases: []string{},
Short: "Artransfer export",
Long: "\n# Artransfer - Export\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html",
Run: func(cmd *cobra.Command, args []string) {
jsonStr, err := artransfer.RunExportArtrans(app.Dao(), &artransfer.ExportParams{})
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
// check dir
stat, err := os.Stat(filename)
if err == nil {
if stat.IsDir() {
filename = path.Join(filename, "backup-"+time.Now().Format("20060102-150405")+".artrans")
if len(args) < 1 || args[0] == "" {
// write to stdout
fmt.Println(jsonStr)
} else {
filename := args[0]
// make sure is abs path
filename, err := filepath.Abs(filename)
if err != nil {
log.Fatal(err)
}
}
// mkdir -p
if err := utils.EnsureDir(filepath.Dir(filename)); err != nil {
logrus.Fatal(err)
}
// check dir
stat, err := os.Stat(filename)
if err == nil {
if stat.IsDir() {
filename = path.Join(filename, "backup-"+time.Now().Format("20060102-150405")+".artrans")
}
}
// touch
f, err := os.Create(filename)
if err != nil {
logrus.Fatal(err)
}
// mkdir -p
if err := utils.EnsureDir(filepath.Dir(filename)); err != nil {
log.Fatal(err)
}
// >
_, err2 := f.WriteString(jsonStr)
if err2 != nil {
logrus.Fatal(err2)
}
// touch
f, err := os.Create(filename)
if err != nil {
log.Fatal(err)
}
logrus.Info(i18n.T("Export complete") + ": " + filename)
}
},
}
// >
_, err2 := f.WriteString(jsonStr)
if err2 != nil {
log.Fatal(err2)
}
func init() {
rootCmd.AddCommand(exportCmd)
log.Info(i18n.T("Export complete") + ": " + filename)
}
},
}
return exportCmd
}
+22 -29
View File
@@ -1,41 +1,34 @@
package cmd
import (
"os"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var genCmd = &cobra.Command{
Use: "gen <TYPE> <DEST>",
Short: "A collection of several useful generators",
Long: "Generate some content\ne.g. `artalk gen config ./artalk.yml`",
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
// change working directory
if workDir != "" {
if err := os.Chdir(workDir); err != nil {
logrus.Fatal("change working directory error ", err)
func NewGenCommand() *cobra.Command {
genCmd := &cobra.Command{
Use: "gen <TYPE> <DEST>",
Short: "A collection of several useful generators",
Long: "Generate some content\ne.g. `artalk gen config ./artalk.yml`",
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
var (
specificPath string
isForce bool
)
if len(args) > 1 {
specificPath = args[1]
}
}
isForce, _ = cmd.Flags().GetBool("force")
var (
specificPath string
isForce bool
)
if len(args) > 1 {
specificPath = args[1]
}
isForce, _ = cmd.Flags().GetBool("force")
core.Gen(args[0], specificPath, isForce)
},
}
func init() {
rootCmd.AddCommand(genCmd)
core.Gen(args[0], specificPath, isForce)
},
Annotations: map[string]string{
BootModeKey: MODE_MINI_BOOT,
},
}
flagPV(genCmd, "force", "f", false, "Force overwrite an existing file")
return genCmd
}
+24 -25
View File
@@ -5,38 +5,37 @@ import (
"os"
"github.com/ArtalkJS/Artalk/internal/artransfer"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/sirupsen/logrus"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/spf13/cobra"
)
var importCmd = &cobra.Command{
Use: "import <FILENAME>",
Aliases: []string{},
Short: "Artransfer - Import",
Long: "\n# Artransfer - Import\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
core.LoadCore(cfgFile, workDir) // load core
func NewImportCommand(app *ArtalkCmd) *cobra.Command {
importCmd := &cobra.Command{
Use: "import <FILENAME>",
Aliases: []string{},
Short: "Artransfer import",
Long: "\n# Artransfer - Import\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
parcelFile := args[0]
if _, err := os.Stat(parcelFile); errors.Is(err, os.ErrNotExist) {
log.Fatal(i18n.T("{{name}} not found", map[string]interface{}{"name": i18n.T("File")}))
}
parcelFile := args[0]
if _, err := os.Stat(parcelFile); errors.Is(err, os.ErrNotExist) {
logrus.Fatal(i18n.T("{{name}} not found", map[string]interface{}{"name": i18n.T("File")}))
}
payload := args[1:]
payload = append(payload, "json_file:"+parcelFile)
payload := args[1:]
payload = append(payload, "json_file:"+parcelFile)
// import Artrans
params := artransfer.ArrToImportParams(payload)
params.Assumeyes, _ = cmd.Flags().GetBool("assumeyes")
artransfer.RunImportArtrans(app.Dao(), params)
// import Artrans
artransfer.RunImportArtrans(payload)
logrus.Info(i18n.T("Import complete"))
},
}
func init() {
rootCmd.AddCommand(importCmd)
log.Info(i18n.T("Import complete"))
},
}
flagPV(importCmd, "assumeyes", "y", false, "Automatically answer yes for all questions.")
return importCmd
}
-77
View File
@@ -1,77 +0,0 @@
package cmd
import (
"encoding/json"
"fmt"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/spf13/cobra"
)
var Version = config.Version + `/` + config.CommitHash
var Banner = `
________ ________ _________ ________ ___ ___ __
|\ __ \|\ __ \|\___ ___\\ __ \|\ \ |\ \|\ \
\ \ \|\ \ \ \|\ \|___ \ \_\ \ \|\ \ \ \ \ \ \/ /|_
\ \ __ \ \ _ _\ \ \ \ \ \ __ \ \ \ \ \ ___ \
\ \ \ \ \ \ \\ \| \ \ \ \ \ \ \ \ \ \____\ \ \\ \ \
\ \__\ \__\ \__\\ _\ \ \__\ \ \__\ \__\ \_______\ \__\\ \__\
\|__|\|__|\|__|\|__| \|__| \|__|\|__|\|_______|\|__| \|__|
Artalk (` + Version + `)
-> A Selfhosted Comment System.
-> https://artalk.js.org
`
var (
cfgFile string
workDir string
)
var rootCmd = &cobra.Command{
Use: "artalk",
Short: "Artalk: A Fast, Slight & Delightful Comment System",
Long: Banner,
Version: Version,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(Banner)
fmt.Print("-------------------------------\n\n")
fmt.Println("NOTE: add `-h` flag to show help about any command.")
},
}
func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
func init() {
rootCmd.SetVersionTemplate("Artalk ({{printf \"%s\" .Version}})\n")
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file path (defaults are './artalk.yml')")
rootCmd.PersistentFlags().StringVarP(&workDir, "workdir", "w", "", "program working directory (defaults are './')")
// Version Command
versionCmd := &cobra.Command{
Use: "version",
Short: "Output Version Information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Artalk (" + Version + ")")
},
}
rootCmd.AddCommand(versionCmd)
// Config Command
configCmd := &cobra.Command{
Use: "config",
Short: "Output Config Information",
Run: func(cmd *cobra.Command, args []string) {
core.LoadConfOnly(cfgFile, workDir)
buf, _ := json.MarshalIndent(config.Instance, "", " ")
fmt.Println(string(buf))
},
}
rootCmd.AddCommand(configCmd)
}
+21 -72
View File
@@ -2,85 +2,34 @@ package cmd
import (
"fmt"
"io"
"os"
"os/signal"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/server"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var serverCmd = &cobra.Command{
Use: "server",
Aliases: []string{"serve"},
Short: "Start the server",
Long: Banner,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
core.LoadCore(cfgFile, workDir)
func NewServeCommand(app *ArtalkCmd) *cobra.Command {
var serverCmd = &cobra.Command{
Use: "server",
Aliases: []string{"serve"},
Short: "Start the server",
Long: Banner,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(Banner)
fmt.Print("-------------------------------\n\n")
fmt.Println(Banner)
fmt.Print("-------------------------------\n\n")
// create fiber app
// @see https://docs.gofiber.io/api/fiber#config
app := fiber.New(fiber.Config{
// @see https://github.com/gofiber/fiber/issues/426
// @see https://github.com/gofiber/fiber/issues/185
Immutable: true,
// TODO add config to set ProxyHeader
ProxyHeader: "X-Forwarded-For",
EnableIPValidation: true,
})
// logger
app.Use(logger.New(logger.Config{
Format: "[${status}] ${method} ${path} ${latency} ${ip} ${reqHeader:X-Request-ID} ${referer} ${ua}\n",
Output: io.Discard,
Done: func(c *fiber.Ctx, logString []byte) {
statusOK := c.Response().StatusCode() >= 200 && c.Response().StatusCode() <= 302
if !statusOK {
logrus.StandardLogger().WriterLevel(logrus.ErrorLevel).Write(logString)
} else {
logrus.StandardLogger().WriterLevel(logrus.DebugLevel).Write(logString)
}
},
}))
// init router
server.Init(app)
// graceful shutdown signal listen
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
<-c
fmt.Println("Gracefully shutting down...")
_ = app.Shutdown()
}()
// socket listen
listenAddr := fmt.Sprintf("%s:%d", config.Instance.Host, config.Instance.Port)
if config.Instance.SSL.Enabled {
app.ListenTLS(listenAddr, config.Instance.SSL.CertPath, config.Instance.SSL.KeyPath)
} else {
app.Listen(listenAddr)
}
// graceful shutdown tasks
fmt.Println("Running cleanup tasks...")
},
}
func init() {
rootCmd.AddCommand(serverCmd)
// init fiber app
_, err := server.Serve(app.App)
if err != nil {
log.Fatalln(err)
}
},
}
flagPV(serverCmd, "host", "", "0.0.0.0", "Listening IP")
flagPV(serverCmd, "port", "", 23366, "Listening port")
return serverCmd
}
+44 -44
View File
@@ -6,63 +6,63 @@ import (
"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/blang/semver"
"github.com/rhysd/go-github-selfupdate/selfupdate"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var upgradeCmd = &cobra.Command{
Use: "upgrade",
Aliases: []string{"update"},
Short: "Upgrade to the latest version",
Long: "Upgrade Artalk to the latest version, \n update source is GitHub Releases, \n update need to restart Artalk to take effect.",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
// loadCore()
core.LoadConfOnly(cfgFile, workDir)
func NewUpgradeCommand() *cobra.Command {
upgradeCmd := &cobra.Command{
Use: "upgrade",
Aliases: []string{"update"},
Short: "Upgrade to the latest version",
Long: "Upgrade Artalk to the latest version, \n update source is GitHub Releases, \n update need to restart Artalk to take effect.",
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
log.Info(i18n.T("Checking for updates") + "...")
logrus.Info(i18n.T("Checking for updates") + "...")
latest, found, err := selfupdate.DetectLatest("ArtalkJS/Artalk")
if err != nil {
logrus.Fatal("Error occurred while detecting version: ", err)
}
ignoreVersionCheck, _ := cmd.Flags().GetBool("force")
if !ignoreVersionCheck {
v := semver.MustParse(strings.TrimPrefix(config.Version, "v"))
if !found || latest.Version.LTE(v) {
logrus.Println(i18n.T("Current version is the latest") + " (v" + v.String() + ")")
return
latest, found, err := selfupdate.DetectLatest("ArtalkJS/Artalk")
if err != nil {
log.Fatal("Error occurred while detecting version: ", err)
}
}
logrus.Info(i18n.T("New version available") + ": v" + latest.Version.String())
logrus.Info(i18n.T("Downloading") + "...")
ignoreVersionCheck, _ := cmd.Flags().GetBool("force")
if !ignoreVersionCheck {
v := semver.MustParse(strings.TrimPrefix(config.Version, "v"))
if !found || latest.Version.LTE(v) {
log.Println(i18n.T("Current version is the latest") + " (v" + v.String() + ")")
return
}
}
exe, err := os.Executable()
if err != nil {
logrus.Fatal("Could not locate executable path ", err)
}
log.Info(i18n.T("New version available") + ": v" + latest.Version.String())
log.Info(i18n.T("Downloading") + "...")
if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil {
logrus.Fatal(i18n.T("Update failed")+" ", err)
}
exe, err := os.Executable()
if err != nil {
log.Fatal("Could not locate executable path ", err)
}
logrus.Println(i18n.T("Update complete"))
fmt.Println("\n-------------------------------\n v" +
latest.Version.String() +
" Release Note\n" +
"-------------------------------\n\n" +
latest.ReleaseNotes)
},
}
if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil {
log.Fatal(i18n.T("Update failed")+" ", err)
}
func init() {
rootCmd.AddCommand(upgradeCmd)
log.Println(i18n.T("Update complete"))
fmt.Printf("\n"+
"-------------------------------\n"+
" v%s Release Note\n"+
"-------------------------------\n\n"+
"%s\n",
latest.Version.String(), latest.ReleaseNotes)
},
Annotations: map[string]string{
BootModeKey: MODE_MINI_BOOT,
},
}
flagPV(upgradeCmd, "force", "f", false, "Force upgrade ignore version comparison.")
return upgradeCmd
}
-39
View File
@@ -1,39 +0,0 @@
package cmd
import (
"github.com/spf13/cobra"
)
//// Shortcut Functions ////
func flag(cmd *cobra.Command, name string, defaultVal interface{}, usage string) {
f := cmd.PersistentFlags()
switch y := defaultVal.(type) {
case bool:
f.Bool(name, y, usage)
case int:
f.Int(name, y, usage)
case string:
f.String(name, y, usage)
}
}
func flagP(cmd *cobra.Command, name, shorthand string, defaultVal interface{}, usage string) {
f := cmd.PersistentFlags()
switch y := defaultVal.(type) {
case bool:
f.BoolP(name, shorthand, y, usage)
case int:
f.IntP(name, shorthand, y, usage)
case string:
f.StringP(name, shorthand, y, usage)
}
}
func flagV(cmd *cobra.Command, name string, defaultVal interface{}, usage string) {
flag(cmd, name, defaultVal, usage)
}
func flagPV(cmd *cobra.Command, name, shorthand string, defaultVal interface{}, usage string) {
flagP(cmd, name, shorthand, defaultVal, usage)
}
+22
View File
@@ -0,0 +1,22 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func NewVersionCommand() *cobra.Command {
versionCmd := &cobra.Command{
Use: "version",
Short: "Output version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Artalk (" + Version + ")")
},
Annotations: map[string]string{
BootModeKey: MODE_MINI_BOOT,
},
}
return versionCmd
}
+2 -1
View File
@@ -20,6 +20,7 @@ log:
enabled: true
filename: "./data/artalk.log"
cache:
enabled: false
type: "builtin"
expires: 30
warm_up: false
@@ -152,7 +153,7 @@ frontend:
nestSort: DATE_ASC
gravatar:
mirror: "https://www.gravatar.com/"
default: "mp"
params: "d=mp&s=240"
pagination:
pageSize: 20
readMore: true
+6 -2
View File
@@ -55,6 +55,8 @@ log:
# Cache
cache:
# Enable cache
enabled: false
# Cache type ["redis", "memcache", "builtin"]
type: "builtin"
# Cache expiration time (in minutes)
@@ -297,8 +299,10 @@ frontend:
nestSort: DATE_ASC
# Gravatar
gravatar:
mirror: "https://www.gravatar.com/"
default: "mp"
# API URL
mirror: "https://www.gravatar.com/avatar/"
# API parameters
params: "d=mp&s=240"
# Comment pagination
pagination:
# Number of comments per page
+6 -4
View File
@@ -52,6 +52,8 @@ log:
# 缓存
cache:
# 启用缓存
enabled: false
# 缓存类型 ["redis", "memcache", "builtin"]
type: "builtin"
# 缓存过期时间 (单位:分钟)
@@ -302,12 +304,12 @@ frontend:
nestMax: 2
# 嵌套评论排序规则 ["DATE_ASC", "DATE_DESC", "VOTE_UP_DESC"]
nestSort: DATE_ASC
# 头像
# 头像 Gravatar
gravatar:
# Gravatar 镜像地址
# API 地址
mirror: "https://cravatar.cn/avatar/"
# 默认头像
default: "mp"
# API 参数
params: "d=mp&s=240"
# 评论分页
pagination:
# 每页评论数
+1
View File
@@ -1 +1,2 @@
shamefully-hoist=true
auto-install-peers=true
+1 -1
View File
@@ -1,3 +1,3 @@
{
"latest": "2.5.2"
"latest": "2.6.2"
}
+19
View File
@@ -12,6 +12,25 @@ Artalk 默认以工作目录下的 `artalk.yml` 作为配置文件,可使用
artalk -c ./conf.yml
```
## 通过环境变量配置
Artalk 读取以 `ATK_` 为前缀的环境变量,并且全部大写,子节点用单个下划线表示,配置名含下划线请用双下划线表示:
- `_` (单下划线) 转为 `.` 表示子节点
- `__` (双下划线) 转为 `_` 表示配置名的下划线
e.g.
```bash
ATK_TIMEZONE -> timezone
ATK_LOGIN__TIMEOUT -> login_timeout
ATK_SITE__DEFAULT -> site_default
ATK_DB_TYPE -> db.type
ATK_DB_TABLE__PREFIX -> db.table_prefix
ATK_CACHE_REDIS_USERNAME -> cache.redis.username
ATK_ADMIN_USERS_0_NAME -> admin_users[0].name
```
## 获取模版配置文件
可参考一份「完整的配置文件」:[artalk.example.zh-CN.yml](https://github.com/ArtalkJS/Artalk/blob/master/conf/artalk.example.zh-CN.yml)
+2
View File
@@ -12,6 +12,8 @@ Artalk 离不开社区的支持与帮助,**是你们让 Artalk 走得更远!
- [Nuxt.js如何部署Artalk和遇到的问题 (By @子舒)](https://imhan.cn/posts/20220218/)
- [宝塔安装过程大概记录 (By @xiamuguizhi)](https://github.com/ArtalkJS/Artalk/discussions/46)
- [Vuepress配置评论插件为Artalk](https://wiki.eryajf.net/pages/b74c2b/)
- [Fly.io 部署 Artalk 评论系统](https://wayjam.me/posts/artalk-deploy-on-flyio/)
- [Railway.app 部署 Artalk 评论系统](https://blog.moraxyc.com/post/eidb8276/)
更多新版读物正在撰写当中(
+15 -3
View File
@@ -350,12 +350,24 @@ gravatar: {
>
> loli:https://gravatar.loli.net/avatar/
### gravatar.default
### gravatar.params
**默认头像**(URL or [Gravatar Type](http://cn.gravatar.org/site/implement/images/#default-image))
**Gravatar API 参数**
- 类型:`String`
- 默认值:`"mp"`
- 默认值:`"d=mp&s=240"`
例如,你可以通过该配置项设置默认头像 (`d=mp`) 和头像尺寸 (`s=240`)。
参考:[Gravatar API 文档](http://cn.gravatar.org/site/implement/images/)
该配置项格式为 HTTP Query。
::: warning 更新注意
v2.5.5 已废弃 `gravatar.default` 配置项,请使用 `gravatar.params` 替代。
:::
### avatarURLBuilder
+1 -1
View File
@@ -64,7 +64,7 @@ Artalk 社区是包容开放的社区,我们欢迎不同水平的人员帮助
“More action, less talk”,Artalk 社区不欢迎无意义的争论,我们希望社区成员和谐共处、为社区发展出谋划策。在提出问题前,你应当读过《[提问的智慧](https://lug.ustc.edu.cn/wiki/doc/smart-questions/)》,这可能决定了你最终是否能得到有用的回答。在表达观点前,你应当具备基本的礼仪,比如保持平和的态度、使用得体的语言,切忌恶语相向、冷嘲热讽、不尊重他人信仰和立场等。
我们作为开源精神的推崇者以及实践者,希望我们所创造的自由软件,都应该被自由的使用,自由的研究,自由的更改和自由的分享。本项目前后端代码使用第 3 版 GNU 宽通用公共许可证([GNU Lesser General Public License V3](https://www.gnu.org/licenses/lgpl-3.0.html))开源,文档请遵循知识共享许可协议 ([Creative Commons License](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh))。
我们作为开源精神的推崇者以及实践者,希望我们所创造的自由软件,都应该被自由的使用,自由的研究,自由的更改和自由的分享。本项目前后端代码使用 [MIT](https://github.com/ArtalkJS/Artalk/blob/master/LICENSE) 协议开源,文档请遵循知识共享许可协议 ([Creative Commons License](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh))。
::: tip 立即为社区贡献力量?
- 浏览开发者资料([开发文档](../develop/index.md) / [CONTRIBUTING.md](https://github.com/ArtalkJS/Artalk/blob/master/CONTRIBUTING.md))
+4 -3
View File
@@ -2,6 +2,7 @@
"name": "docs",
"version": "2.0.0",
"description": "Artalk documentation",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/ArtalkJS/Artalk",
@@ -16,9 +17,9 @@
"swagger:serve": "npx @redocly/cli preview-docs"
},
"devDependencies": {
"@redocly/cli": "1.0.0-beta.124",
"@redocly/cli": "1.0.2",
"markdown-it-for-inline": "^0.1.1",
"sass": "^1.59.3",
"vitepress": "1.0.0-alpha.60"
"sass": "^1.66.1",
"vitepress": "1.0.0-rc.4"
}
}
+1354 -680
View File
File diff suppressed because it is too large. Load diff
+112 -4
View File
@@ -1827,9 +1827,16 @@ const docTemplate = `{
],
"summary": "Notify Mark Read",
"parameters": [
{
"type": "integer",
"description": "the comment id of the notify you want to mark as read",
"name": "comment_id",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "the notify key you want to mark as read",
"description": "the key of the notify",
"name": "notify_key",
"in": "formData",
"required": true
@@ -2120,9 +2127,6 @@ const docTemplate = `{
"commit_hash": {
"type": "string"
},
"fe_min_version": {
"type": "string"
},
"version": {
"type": "string"
}
@@ -2373,14 +2377,43 @@ const docTemplate = `{
"always": {
"type": "boolean"
},
"captcha_type": {
"$ref": "#/definitions/config.CaptchaType"
},
"enabled": {
"type": "boolean"
},
"geetest": {
"$ref": "#/definitions/config.GeetestConf"
},
"hcaptcha": {
"$ref": "#/definitions/config.HCaptchaConf"
},
"recaptcha": {
"$ref": "#/definitions/config.ReCaptchaConf"
},
"turnstile": {
"$ref": "#/definitions/config.TurnstileConf"
}
}
},
"config.CaptchaType": {
"type": "string",
"enum": [
"image",
"turnstile",
"recaptcha",
"hcaptcha",
"geetest"
],
"x-enum-varnames": [
"TypeImage",
"TypeTurnstile",
"TypeReCaptcha",
"TypeHCaptcha",
"TypeGeetest"
]
},
"config.Config": {
"type": "object",
"properties": {
@@ -2463,6 +2496,14 @@ const docTemplate = `{
}
]
},
"ip_region": {
"description": "IP 归属地展示",
"allOf": [
{
"$ref": "#/definitions/config.IPRegionConf"
}
]
},
"locale": {
"description": "语言",
"type": "string"
@@ -2598,6 +2639,14 @@ const docTemplate = `{
"description": "邮件模板",
"type": "string"
},
"queue": {
"description": "邮件发送队列配置",
"allOf": [
{
"$ref": "#/definitions/config.EmailQueueConf"
}
]
},
"send_addr": {
"description": "发件人地址",
"type": "string"
@@ -2624,6 +2673,15 @@ const docTemplate = `{
}
}
},
"config.EmailQueueConf": {
"type": "object",
"properties": {
"buffer_size": {
"description": "Channel buffer size (default is zero that not create buffer)",
"type": "integer"
}
}
},
"config.EmailSenderType": {
"type": "string",
"enum": [
@@ -2645,9 +2703,34 @@ const docTemplate = `{
},
"captcha_key": {
"type": "string"
}
}
},
"config.HCaptchaConf": {
"type": "object",
"properties": {
"secret_key": {
"type": "string"
},
"site_key": {
"type": "string"
}
}
},
"config.IPRegionConf": {
"type": "object",
"properties": {
"db_path": {
"description": "数据文件路径",
"type": "string"
},
"enabled": {
"description": "启用 IP 归属地展示",
"type": "boolean"
},
"precision": {
"description": "显示精度",
"type": "string"
}
}
},
@@ -2843,6 +2926,17 @@ const docTemplate = `{
}
}
},
"config.ReCaptchaConf": {
"type": "object",
"properties": {
"secret_key": {
"type": "string"
},
"site_key": {
"type": "string"
}
}
},
"config.RedisConf": {
"type": "object",
"properties": {
@@ -2913,6 +3007,17 @@ const docTemplate = `{
}
}
},
"config.TurnstileConf": {
"type": "object",
"properties": {
"secret_key": {
"type": "string"
},
"site_key": {
"type": "string"
}
}
},
"config.UpgitConf": {
"type": "object",
"properties": {
@@ -2954,6 +3059,9 @@ const docTemplate = `{
"id": {
"type": "integer"
},
"ip_region": {
"type": "string"
},
"is_allow_reply": {
"type": "boolean"
},
+112 -4
View File
@@ -1820,9 +1820,16 @@
],
"summary": "Notify Mark Read",
"parameters": [
{
"type": "integer",
"description": "the comment id of the notify you want to mark as read",
"name": "comment_id",
"in": "formData",
"required": true
},
{
"type": "string",
"description": "the notify key you want to mark as read",
"description": "the key of the notify",
"name": "notify_key",
"in": "formData",
"required": true
@@ -2113,9 +2120,6 @@
"commit_hash": {
"type": "string"
},
"fe_min_version": {
"type": "string"
},
"version": {
"type": "string"
}
@@ -2366,14 +2370,43 @@
"always": {
"type": "boolean"
},
"captcha_type": {
"$ref": "#/definitions/config.CaptchaType"
},
"enabled": {
"type": "boolean"
},
"geetest": {
"$ref": "#/definitions/config.GeetestConf"
},
"hcaptcha": {
"$ref": "#/definitions/config.HCaptchaConf"
},
"recaptcha": {
"$ref": "#/definitions/config.ReCaptchaConf"
},
"turnstile": {
"$ref": "#/definitions/config.TurnstileConf"
}
}
},
"config.CaptchaType": {
"type": "string",
"enum": [
"image",
"turnstile",
"recaptcha",
"hcaptcha",
"geetest"
],
"x-enum-varnames": [
"TypeImage",
"TypeTurnstile",
"TypeReCaptcha",
"TypeHCaptcha",
"TypeGeetest"
]
},
"config.Config": {
"type": "object",
"properties": {
@@ -2456,6 +2489,14 @@
}
]
},
"ip_region": {
"description": "IP 归属地展示",
"allOf": [
{
"$ref": "#/definitions/config.IPRegionConf"
}
]
},
"locale": {
"description": "语言",
"type": "string"
@@ -2591,6 +2632,14 @@
"description": "邮件模板",
"type": "string"
},
"queue": {
"description": "邮件发送队列配置",
"allOf": [
{
"$ref": "#/definitions/config.EmailQueueConf"
}
]
},
"send_addr": {
"description": "发件人地址",
"type": "string"
@@ -2617,6 +2666,15 @@
}
}
},
"config.EmailQueueConf": {
"type": "object",
"properties": {
"buffer_size": {
"description": "Channel buffer size (default is zero that not create buffer)",
"type": "integer"
}
}
},
"config.EmailSenderType": {
"type": "string",
"enum": [
@@ -2638,9 +2696,34 @@
},
"captcha_key": {
"type": "string"
}
}
},
"config.HCaptchaConf": {
"type": "object",
"properties": {
"secret_key": {
"type": "string"
},
"site_key": {
"type": "string"
}
}
},
"config.IPRegionConf": {
"type": "object",
"properties": {
"db_path": {
"description": "数据文件路径",
"type": "string"
},
"enabled": {
"description": "启用 IP 归属地展示",
"type": "boolean"
},
"precision": {
"description": "显示精度",
"type": "string"
}
}
},
@@ -2836,6 +2919,17 @@
}
}
},
"config.ReCaptchaConf": {
"type": "object",
"properties": {
"secret_key": {
"type": "string"
},
"site_key": {
"type": "string"
}
}
},
"config.RedisConf": {
"type": "object",
"properties": {
@@ -2906,6 +3000,17 @@
}
}
},
"config.TurnstileConf": {
"type": "object",
"properties": {
"secret_key": {
"type": "string"
},
"site_key": {
"type": "string"
}
}
},
"config.UpgitConf": {
"type": "object",
"properties": {
@@ -2947,6 +3052,9 @@
"id": {
"type": "integer"
},
"ip_region": {
"type": "string"
},
"is_allow_reply": {
"type": "boolean"
},
+75 -3
View File
@@ -6,8 +6,6 @@ definitions:
type: string
commit_hash:
type: string
fe_min_version:
type: string
version:
type: string
type: object
@@ -168,11 +166,33 @@ definitions:
type: integer
always:
type: boolean
captcha_type:
$ref: '#/definitions/config.CaptchaType'
enabled:
type: boolean
geetest:
$ref: '#/definitions/config.GeetestConf'
hcaptcha:
$ref: '#/definitions/config.HCaptchaConf'
recaptcha:
$ref: '#/definitions/config.ReCaptchaConf'
turnstile:
$ref: '#/definitions/config.TurnstileConf'
type: object
config.CaptchaType:
enum:
- image
- turnstile
- recaptcha
- hcaptcha
- geetest
type: string
x-enum-varnames:
- TypeImage
- TypeTurnstile
- TypeReCaptcha
- TypeHCaptcha
- TypeGeetest
config.Config:
properties:
admin_notify:
@@ -220,6 +240,10 @@ definitions:
allOf:
- $ref: '#/definitions/config.ImgUploadConf'
description: 图片上传
ip_region:
allOf:
- $ref: '#/definitions/config.IPRegionConf'
description: IP 归属地展示
locale:
description: 语言
type: string
@@ -309,6 +333,10 @@ definitions:
mail_tpl:
description: 邮件模板
type: string
queue:
allOf:
- $ref: '#/definitions/config.EmailQueueConf'
description: 邮件发送队列配置
send_addr:
description: 发件人地址
type: string
@@ -324,6 +352,12 @@ definitions:
- $ref: '#/definitions/config.SMTPConf'
description: SMTP 配置
type: object
config.EmailQueueConf:
properties:
buffer_size:
description: Channel buffer size (default is zero that not create buffer)
type: integer
type: object
config.EmailSenderType:
enum:
- smtp
@@ -340,8 +374,25 @@ definitions:
type: string
captcha_key:
type: string
type: object
config.HCaptchaConf:
properties:
secret_key:
type: string
site_key:
type: string
type: object
config.IPRegionConf:
properties:
db_path:
description: 数据文件路径
type: string
enabled:
description: 启用 IP 归属地展示
type: boolean
precision:
description: 显示精度
type: string
type: object
config.ImgUploadConf:
properties:
@@ -468,6 +519,13 @@ definitions:
url:
type: string
type: object
config.ReCaptchaConf:
properties:
secret_key:
type: string
site_key:
type: string
type: object
config.RedisConf:
properties:
db:
@@ -514,6 +572,13 @@ definitions:
secret_key:
type: string
type: object
config.TurnstileConf:
properties:
secret_key:
type: string
site_key:
type: string
type: object
config.UpgitConf:
properties:
del_local:
@@ -542,6 +607,8 @@ definitions:
type: string
id:
type: integer
ip_region:
type: string
is_allow_reply:
type: boolean
is_collapsed:
@@ -1905,7 +1972,12 @@ paths:
post:
description: Mark specific notification as read for user
parameters:
- description: the notify key you want to mark as read
- description: the comment id of the notify you want to mark as read
in: formData
name: comment_id
required: true
type: integer
- description: the key of the notify
in: formData
name: notify_key
required: true
+68 -63
View File
@@ -1,56 +1,59 @@
module github.com/ArtalkJS/Artalk
go 1.20
go 1.21
require (
github.com/allegro/bigcache/v3 v3.1.0
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/blang/semver v3.5.1+incompatible
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746
github.com/cheggaaa/pb/v3 v3.1.2
github.com/eko/gocache/lib/v4 v4.1.3
github.com/eko/gocache/store/bigcache/v4 v4.1.2
github.com/eko/gocache/store/memcache/v4 v4.1.2
github.com/eko/gocache/store/redis/v4 v4.1.2
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc
github.com/go-testfixtures/testfixtures/v3 v3.8.1
github.com/gofiber/fiber/v2 v2.42.0
github.com/gofiber/swagger v0.1.9
github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285
github.com/cheggaaa/pb/v3 v3.1.4
github.com/eko/gocache/lib/v4 v4.1.4
github.com/eko/gocache/store/bigcache/v4 v4.2.0
github.com/eko/gocache/store/memcache/v4 v4.2.0
github.com/eko/gocache/store/redis/v4 v4.2.0
github.com/fatih/color v1.15.0
github.com/go-testfixtures/testfixtures/v3 v3.9.0
github.com/gofiber/fiber/v2 v2.48.0
github.com/gofiber/swagger v0.1.12
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/jedib0t/go-pretty/v6 v6.4.6
github.com/jeremywohl/flatten v1.0.1
github.com/knadh/koanf v1.5.0
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230205110531-05840c74e63c
github.com/microcosm-cc/bluemonday v1.0.23
github.com/nikoksr/notify v0.38.1
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230731060429-6ed8bf011875
github.com/microcosm-cc/bluemonday v1.0.25
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.1.0
github.com/rhysd/go-github-selfupdate v1.2.3
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/samber/lo v1.37.0
github.com/sirupsen/logrus v1.9.0
github.com/spf13/cobra v1.6.1
github.com/samber/lo v1.38.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/steambap/captcha v1.4.1
github.com/stretchr/testify v1.8.2
github.com/swaggo/swag v1.8.10
github.com/tidwall/gjson v1.14.4
github.com/stretchr/testify v1.8.4
github.com/swaggo/swag v1.16.1
github.com/tidwall/gjson v1.16.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.4
golang.org/x/crypto v0.7.0
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0
golang.org/x/sync v0.1.0
golang.org/x/term v0.6.0
github.com/yuin/goldmark v1.5.6
golang.org/x/crypto v0.12.0
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
golang.org/x/sync v0.3.0
golang.org/x/term v0.11.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.4.7
gorm.io/driver/postgres v1.5.0
gorm.io/driver/sqlite v1.4.4
gorm.io/driver/sqlserver v1.4.2
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11
gorm.io/driver/mysql v1.5.1
gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.5.3
gorm.io/driver/sqlserver v1.5.1
gorm.io/gorm v1.25.4
)
require (
github.com/ClickHouse/ch-go v0.58.2 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.13.2 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
@@ -59,15 +62,15 @@ require (
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denisenkom/go-mssqldb v0.12.3 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // 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.8 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.4 // 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
github.com/golang-sql/sqlexp v0.1.0 // indirect
@@ -83,56 +86,58 @@ require (
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/v4 v4.18.1 // indirect
github.com/jackc/pgx/v5 v5.3.1 // indirect
github.com/jackc/pgx/v5 v5.4.3 // 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.16.3 // indirect
github.com/klauspost/compress v1.16.7 // 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.17 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mattn/go-isatty v0.0.19 // 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/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/microsoft/go-mssqldb v0.20.0 // indirect
github.com/microsoft/go-mssqldb v1.5.0 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/onsi/gomega v1.27.4 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.27.10 // indirect
github.com/paulmach/orb v0.10.0 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.14.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/slack-go/slack v0.12.1 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/slack-go/slack v0.12.2 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/swaggo/files v1.0.0 // indirect
github.com/stretchr/objx v0.5.1 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
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.45.0 // indirect
github.com/valyala/fasthttp v1.48.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/image v0.6.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/tools v0.7.0 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
golang.org/x/image v0.11.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/oauth2 v0.11.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
)
+191 -247
View File
@@ -1,17 +1,16 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
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.13.2 h1:LSg6670+xbd5VczO5Ei3DHZBIeGulfwhNuHCUth/qOA=
github.com/ClickHouse/clickhouse-go/v2 v2.13.2/go.mod h1:4QITCrdY/ugPYA+QGnJ92h+v7TGaZQ7l0393Q/wlM3Q=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
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=
@@ -23,7 +22,6 @@ 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.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -56,25 +54,25 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 h1:pamuM2sgLJLoMWfchc6y071z8ifalajU7btZmZNhoH4=
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746 h1:wAIE/kN63Oig1DdOzN7O+k4AbFh2cCJoKMFXrwRJtzk=
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285 h1:Dr+ezPI5ivhMn/3WOoB86XzMhie146DNaBbhaQWZHMY=
github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0=
github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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.2 h1:FIxT3ZjOj9XJl0U4o2XbEhjFfZl7jCVCDOGq1ZAB7wQ=
github.com/cheggaaa/pb/v3 v3.1.2/go.mod h1:SNjnd0yKcW+kw0brSusraeDd5Bf1zBfxAzTL2ss3yQ4=
github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo=
github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA=
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/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
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/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
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=
@@ -86,14 +84,14 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
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=
github.com/eko/gocache/lib/v4 v4.1.3 h1:i7xVxWWhAESLAcHgp73piJtST63TS7ThFD7LE78vmXc=
github.com/eko/gocache/lib/v4 v4.1.3/go.mod h1:7uSUlaDeRgr17IYCV8kSluhRDXq4SGutWWxTpxpwW3w=
github.com/eko/gocache/store/bigcache/v4 v4.1.2 h1:8uMDpgxTG7BvyLHBFqL3Ao809bVrXfrWqo7v6ALiwTw=
github.com/eko/gocache/store/bigcache/v4 v4.1.2/go.mod h1:Lut5Sk/yI835w02tmwx4ecezYQo445L5sdICsk1zgho=
github.com/eko/gocache/store/memcache/v4 v4.1.2 h1:ke3OolL6a/fgoSAvuRD42q6Q/80hDnbDrZeIjjguDlg=
github.com/eko/gocache/store/memcache/v4 v4.1.2/go.mod h1:0raU4CSQgzihTahA4dxrxsafHt8j2NH1ATL33lSDpX4=
github.com/eko/gocache/store/redis/v4 v4.1.2 h1:PtpjQu8Q698kpC3H06As5As15s+hJofV8NtkddVX5aQ=
github.com/eko/gocache/store/redis/v4 v4.1.2/go.mod h1:P2HeqvNwqkdwajYro+qwBWquhdYpN0LgfH2rJ3jK7yg=
github.com/eko/gocache/lib/v4 v4.1.4 h1:09mjQwg9iKm0D9ue58LRCeMFxCemdL/Cn1zEzQJopA4=
github.com/eko/gocache/lib/v4 v4.1.4/go.mod h1:7uSUlaDeRgr17IYCV8kSluhRDXq4SGutWWxTpxpwW3w=
github.com/eko/gocache/store/bigcache/v4 v4.2.0 h1:oWPW/lPmGe2V91iQkSUa0QcWqIIH9U058hKhG0mnsOk=
github.com/eko/gocache/store/bigcache/v4 v4.2.0/go.mod h1:lAFv7vJHo9Oy5a6u9uVFmoUZKtH9H/KkH2mzy3Al1Xo=
github.com/eko/gocache/store/memcache/v4 v4.2.0 h1:yG+SkTfFLLqO3pWr5gj++fWWy/1KjWg//OThAFavJeQ=
github.com/eko/gocache/store/memcache/v4 v4.2.0/go.mod h1:LM41TOGwHXOs48p1iwP9VUWd7ClX3HHvFLFeIuQBkjI=
github.com/eko/gocache/store/redis/v4 v4.2.0 h1:YguIDZuMeVFlQFi0DiYQpL7NvHAckinYFjWxvr0qmLw=
github.com/eko/gocache/store/redis/v4 v4.2.0/go.mod h1:IP4GWCjca/pyruK5GweTZ7zzoFlgQABd3cVMJFEgHfs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -109,6 +107,10 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
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-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=
@@ -118,44 +120,45 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
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 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
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/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/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU=
github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
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/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 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc h1:jZY+lpZB92nvBo2f31oPC/ivGll6NcsnEOORm8Fkr4M=
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc/go.mod h1:25mL1NKxbJhB63ihiK8MnNeTRd+xAizd6bOdydrTLUQ=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
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-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=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-testfixtures/testfixtures/v3 v3.8.1 h1:uonwvepqRvSgddcrReZQhojTlWlmOlHkYAb9ZaOMWgU=
github.com/go-testfixtures/testfixtures/v3 v3.8.1/go.mod h1:Kdu7YeMC0KRXVHdaQ91Vmx3pcjoTF63h4f1qTJDdXLA=
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.42.0 h1:Fnp7ybWvS+sjNQsFvkhf4G8OhXswvB6Vee8hM/LyS+8=
github.com/gofiber/fiber/v2 v2.42.0/go.mod h1:3+SGNjqMh5VQH5Vz2Wdi43zTIV16ktlFd3x3R6O1Zlc=
github.com/gofiber/swagger v0.1.9 h1:JcUVtxa9cOQdQ0DdLwTA0u2QyM5d2/D/3fUZqBGpYR4=
github.com/gofiber/swagger v0.1.9/go.mod h1:IBHyqGmqbfOwbZmt2X5it5m6PfgtB05VjMN3zfRmY1Y=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc=
github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0=
github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8=
github.com/gofiber/swagger v0.1.12 h1:1Son/Nc1teiIftsVu6UHqXnJ3uf31pUzZO6XQDx3QYs=
github.com/gofiber/swagger v0.1.12/go.mod h1:iOCNEt1gNTtlvCEKoxYX4agnZNtxlAjhujMKG6pmG74=
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.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -170,7 +173,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
@@ -196,15 +198,14 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
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-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=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v0.0.0-20171113160352-8c31c18f31ed/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1/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 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -239,6 +240,7 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
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/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=
@@ -257,67 +259,31 @@ github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEF
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
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-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
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/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
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.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
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.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
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.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
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.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
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.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw=
github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
@@ -325,14 +291,15 @@ github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbg
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@@ -343,73 +310,67 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
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 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
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=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
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.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
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=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
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-20230205110531-05840c74e63c h1:s1xqzuKdzMJlCIh3TD36lZKGfLD4KT340s/NbrXgRIw=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230205110531-05840c74e63c/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230731060429-6ed8bf011875 h1:SeCWAUH2JKORWw0q0/kic8PiHXf6KfADToUQYGHyA2I=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230731060429-6ed8bf011875/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=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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-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.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
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/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.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
github.com/microsoft/go-mssqldb v0.20.0 h1:jAfvs5TIR311fEuNgohFzMtUO0IcCHyI1kWAVws4ES8=
github.com/microsoft/go-mssqldb v0.20.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
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/microsoft/go-mssqldb v1.1.0/go.mod h1:LzkFdl4z2Ck+Hi+ycGOTbL56VEfgoyA2DvYejrNGbRk=
github.com/microsoft/go-mssqldb v1.5.0 h1:CgENxkwtOBNj3Jg6T1X209y2blCfTTcwuOlznd2k9fk=
github.com/microsoft/go-mssqldb v1.5.0/go.mod h1:lmWsjHD8XX/Txr0f8ZqgbEZSC+BZjmEQy/Ms+rLrvho=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
@@ -433,30 +394,39 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nikoksr/notify v0.38.1 h1:+WjA3nUMMhfxKuFFYmTIgDOykdI7GPP3ZWWg3SLuQyo=
github.com/nikoksr/notify v0.38.1/go.mod h1:BA0LnpzG+iBlnxtPnSmV/Ei57wqEtyv9V9IJ+rDlo58=
github.com/nikoksr/notify v0.41.0 h1:4LGE41GpWdHX5M3Xo6DlWRwS2WLDbOq1Rk7IzY4vjmQ=
github.com/nikoksr/notify v0.41.0/go.mod h1:FoE0UVPeopz1Vy5nm9vQZ+JVmYjEIjQgbFstbkw+cRE=
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
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.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
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/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.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
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/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
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/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20161029093637-248dadf4e906/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -472,27 +442,29 @@ 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.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
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=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
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.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
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/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=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
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.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
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=
@@ -503,50 +475,44 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/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.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
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/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.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
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/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw=
github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
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.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ=
github.com/slack-go/slack v0.12.2/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/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=
github.com/steambap/captcha v1.4.1/go.mod h1:oC9T7IfEgnrhzjDz5Djf1H7GPffCzRMbsQfFkJmhlnk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -557,26 +523,26 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
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 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
@@ -586,73 +552,59 @@ 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.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA=
github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
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/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA=
github.com/yuin/goldmark v1.5.6/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.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
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-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
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 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
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=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -660,7 +612,9 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
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 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
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=
@@ -673,35 +627,32 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
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.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
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.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
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=
@@ -713,8 +664,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
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=
@@ -726,12 +678,13 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -741,6 +694,7 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -751,68 +705,61 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-20220728004956-3c1f35247d10/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.2.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 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.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 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
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 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
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=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
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 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
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=
@@ -847,8 +794,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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=
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=
@@ -859,12 +807,9 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
@@ -883,19 +828,18 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/driver/sqlserver v1.4.2 h1:nMtEeKqv2R/vv9FoHUFWfXfP6SskAgRar0TPlZV1stk=
gorm.io/driver/sqlserver v1.4.2/go.mod h1:XHwBuB4Tlh7DqO0x7Ema8dmyWsQW7wi38VQOAFkrbXY=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g=
gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/driver/sqlserver v1.5.1 h1:wpyW/pR26U94uaujltiFGXY7fd2Jw5hC9PB1ZF/Y5s4=
gorm.io/driver/sqlserver v1.5.1/go.mod h1:AYHzzte2msKTmYBYsSIq8ZUsznLJwBdkB2wpI+kt0nM=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
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=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
@@ -1,7 +1,6 @@
package anti_spam
import (
"errors"
"fmt"
"io"
"net/http"
@@ -9,33 +8,31 @@ import (
"reflect"
"strings"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/sirupsen/logrus"
"github.com/ArtalkJS/Artalk/internal/log"
)
type AkismetParams struct {
Blog string `name:"blog"` // required
UserIP string `name:"user_ip"` // required
UserAgent string `name:"user_agent"` // required
var _ Checker = (*AkismetChecker)(nil)
CommentType string `name:"comment_type"`
CommentAuthor string `name:"comment_author"`
CommentAuthorEmail string `name:"comment_author_email"`
CommentAuthorURL string `name:"comment_author_url"`
CommentContent string `name:"comment_content"`
UserRole string `name:"user_role"`
Referrer string `name:"referrer"`
Permalink string `name:"permalink"`
BlogLang string `name:"blog_lang"`
BlogCharset string `name:"blog_charset"`
type AkismetChecker struct {
key string
}
// @link https://akismet.com/development/api/#comment-check
func Akismet(params *AkismetParams, key string) (isPass bool, err error) {
func NewAkismetChecker(key string) Checker {
return &AkismetChecker{
key: key,
}
}
func (*AkismetChecker) Name() string {
return "akismet"
}
func (c *AkismetChecker) Check(p *CheckerParams) (bool, error) {
// @link https://akismet.com/development/api/#comment-check
form := url.Values{}
v := reflect.ValueOf(*params)
reqParams := newAkismetReqParams(p)
v := reflect.ValueOf(*reqParams)
t := v.Type()
for i := 0; i < v.Type().NumField(); i++ {
if v.Field(i).String() != "" {
@@ -46,7 +43,7 @@ func Akismet(params *AkismetParams, key string) (isPass bool, err error) {
client := &http.Client{}
reqBody := strings.NewReader(form.Encode())
api := fmt.Sprintf("https://%s.rest.akismet.com/1.1/comment-check", key)
api := fmt.Sprintf("https://%s.rest.akismet.com/1.1/comment-check", c.key)
req, err := http.NewRequest("POST", api, reqBody)
if err != nil {
return false, err
@@ -67,20 +64,47 @@ func Akismet(params *AkismetParams, key string) (isPass bool, err error) {
respStr := string(respBody)
if config.Instance.Debug {
logrus.Info("akismet Spam Detection Response ", respStr)
}
log.Debug("akismet Spam Detection Response ", respStr)
switch respStr {
case "true":
// is a spam comment
isPass = false
return isPass, nil
return false, nil
case "false":
// not a spam comment
isPass = true
return isPass, nil
return true, nil
}
return false, errors.New(respStr)
return false, fmt.Errorf(respStr)
}
type AkismetReqParams struct {
Blog string `name:"blog"` // required
UserIP string `name:"user_ip"` // required
UserAgent string `name:"user_agent"` // required
CommentType string `name:"comment_type"`
CommentAuthor string `name:"comment_author"`
CommentAuthorEmail string `name:"comment_author_email"`
CommentAuthorURL string `name:"comment_author_url"`
CommentContent string `name:"comment_content"`
UserRole string `name:"user_role"`
Referrer string `name:"referrer"`
Permalink string `name:"permalink"`
BlogLang string `name:"blog_lang"`
BlogCharset string `name:"blog_charset"`
}
func newAkismetReqParams(params *CheckerParams) *AkismetReqParams {
return &AkismetReqParams{
Blog: params.BlogURL,
UserIP: params.UserIP,
UserAgent: params.UserAgent,
CommentAuthor: params.UserName,
CommentAuthorEmail: params.UserEmail,
CommentContent: params.Content,
}
}
+40
View File
@@ -0,0 +1,40 @@
package anti_spam
import (
"fmt"
"github.com/ArtalkJS/Artalk/internal/cloud/aliyun"
)
var _ Checker = (*AliyunChecker)(nil)
type AliyunChecker struct {
AccessKeyID string
AccessKeySecret string
Region string
}
func NewAliyunChecker(accessKeyID, accessKeySecret, region string) Checker {
return &AliyunChecker{
AccessKeyID: accessKeyID,
AccessKeySecret: accessKeySecret,
Region: region,
}
}
func (*AliyunChecker) Name() string {
return "aliyun"
}
// 阿里云反垃圾
// @link https://help.aliyun.com/document_detail/70409.html
// @link https://help.aliyun.com/document_detail/107743.html 接入地址
func (c *AliyunChecker) Check(p *CheckerParams) (bool, error) {
return aliyun.GreenText(aliyun.GreenTextConf{
AccessKeyId: c.AccessKeyID,
AccessKeySecret: c.AccessKeySecret,
Region: c.Region,
Content: p.Content,
DataID: fmt.Sprintf("comment-%d", p.CommentID),
})
}
-162
View File
@@ -1,162 +0,0 @@
package anti_spam
import (
"fmt"
"net/url"
"os"
"strconv"
"strings"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/query"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
)
var AntiSpamReplaceKeywords *[]string
const LOG_TAG = "[Spam Interception] "
func SyncSpamCheck(comment *entity.Comment, fiberCtx *fiber.Ctx) {
// 拦截评论
BlockCommentBy := func(blocker string) {
logrus.Info(fmt.Sprintf(LOG_TAG+"%s Successful blocking of comments ID=%d CONT=%s", blocker, comment.ID, strconv.Quote(comment.Content)))
if comment.IsPending {
return
}
comment.IsPending = true // 改为待审状态
query.UpdateComment(comment)
}
// 拦截失败处理
BlockFailBy := func(blocker string, err error) {
logrus.Error(fmt.Sprintf(LOG_TAG+"%s Interception error occurred ID=%d Err: %s", blocker, comment.ID, strconv.Quote(comment.Content)), err)
}
// 统一拦截处理
ApiCommonHandle := func(blocker string, isPass bool, err error) {
// ApiFailBlock mode
isApiFailBlock := config.Instance.Moderator.ApiFailBlock
if err != nil {
// Api 发生错误
BlockFailBy(blocker, err) // 报告错误
if isApiFailBlock {
BlockCommentBy(blocker) // 仍然拦截
}
} else if !isPass {
// Api 未发生错误,并且 not pass
BlockCommentBy(blocker) // 拦截评论
}
}
// Prepare data for Spam-Check
user := query.FetchUserForComment(comment)
siteURL := ""
if comment.SiteName != "" {
site := query.FindSite(comment.SiteName)
siteURL = query.CookSite(&site).FirstUrl
}
if siteURL == "" { // 从 referer 中提取网站
if pr, err := url.Parse(string(fiberCtx.Request().Header.Referer())); err == nil && pr.Scheme != "" && pr.Host != "" {
siteURL = fmt.Sprintf("%s://%s", pr.Scheme, pr.Host)
}
}
// Akismet
akismetKey := strings.TrimSpace(config.Instance.Moderator.AkismetKey)
if akismetKey != "" {
isPass, err := Akismet(&AkismetParams{
Blog: siteURL,
UserIP: fiberCtx.IP(),
UserAgent: string(fiberCtx.Request().Header.UserAgent()),
CommentType: "comment",
CommentAuthor: user.Name,
CommentAuthorEmail: user.Email,
CommentContent: comment.Content,
}, akismetKey)
ApiCommonHandle("Akismet", isPass, err)
}
// 腾讯云
tencentConf := config.Instance.Moderator.Tencent
if tencentConf.Enabled {
isPass, err := Tencent(TencentParams{
SecretID: tencentConf.SecretID,
SecretKey: tencentConf.SecretKey,
Region: tencentConf.Region,
Content: comment.Content,
CommentID: comment.ID,
UserID: comment.UserID,
UserIP: comment.IP,
UserName: user.Name,
})
ApiCommonHandle("腾讯云", isPass, err)
}
// 阿里云
aliyunConf := config.Instance.Moderator.Aliyun
if aliyunConf.Enabled {
isPass, err := Aliyun(AliyunParams{
AccessKeyID: aliyunConf.AccessKeyID,
AccessKeySecret: aliyunConf.AccessKeySecret,
Region: aliyunConf.Region,
Content: comment.Content,
CommentID: comment.ID,
})
ApiCommonHandle("阿里云", isPass, err)
}
// 关键字过滤
keywordsConf := config.Instance.Moderator.Keywords
if keywordsConf.Enabled {
// 懒加载,初始化
if AntiSpamReplaceKeywords == nil {
AntiSpamReplaceKeywords = &[]string{}
// 加载文件
for _, f := range keywordsConf.Files {
buf, err := os.ReadFile(f)
if err != nil {
logrus.Error("Failed to load Keyword Dictionary file:" + f)
} else {
fileContent := string(buf)
*AntiSpamReplaceKeywords = append(*AntiSpamReplaceKeywords, utils.SplitAndTrimSpace(fileContent, keywordsConf.FileSep)...)
}
}
}
// 关键词过滤
handleContent := comment.Content
replaced := false
for _, keyword := range *AntiSpamReplaceKeywords {
if strings.Contains(handleContent, keyword) {
if keywordsConf.Pending {
BlockCommentBy("Keyword")
break
}
if keywordsConf.ReplacTo != "" {
handleContent = strings.Replace(handleContent, keyword, strings.Repeat(keywordsConf.ReplacTo, len([]rune(keyword))), -1)
replaced = true
}
}
}
if !keywordsConf.Pending && replaced && keywordsConf.ReplacTo != "" {
logrus.Info(fmt.Sprintf(LOG_TAG+"Keyword Replacement Comments ID=%d Original=%s Processed=%s", comment.ID, strconv.Quote(comment.Content), strconv.Quote(handleContent)))
// 保存评论
comment.Content = handleContent
query.UpdateComment(comment)
}
}
}
-29
View File
@@ -1,29 +0,0 @@
package anti_spam
import (
"fmt"
"github.com/ArtalkJS/Artalk/internal/cloud/aliyun"
)
type AliyunParams struct {
AccessKeyID string
AccessKeySecret string
Region string
Content string
CommentID uint
}
// 阿里云反垃圾
// @link https://help.aliyun.com/document_detail/70409.html
// @link https://help.aliyun.com/document_detail/107743.html 接入地址
func Aliyun(p AliyunParams) (isPass bool, err error) {
return aliyun.GreenText(aliyun.GreenTextConf{
AccessKeyId: p.AccessKeyID,
AccessKeySecret: p.AccessKeySecret,
Region: p.Region,
Content: p.Content,
DataID: fmt.Sprintf("comment-%d", p.CommentID),
})
}
+42
View File
@@ -0,0 +1,42 @@
package anti_spam
import (
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/dao"
"github.com/ArtalkJS/Artalk/internal/entity"
)
const LOG_TAG = "[Spam Interception]"
type AntiSpamConf struct {
config.ModeratorConf
Dao *dao.Dao
}
type AntiSpam struct {
conf *AntiSpamConf
dao *dao.Dao
}
func NewAntiSpam(conf *AntiSpamConf) *AntiSpam {
return &AntiSpam{
conf: conf,
dao: conf.Dao,
}
}
type CheckData struct {
Comment *entity.Comment
ReqReferer string
ReqIP string
ReqUserAgent string
}
func (as AntiSpam) CheckAndBlock(data *CheckData) {
checkers := as.getEnabledCheckers()
// 执行检查
for _, checker := range checkers {
as.checkerTrigger(checker, data)
}
}
+21
View File
@@ -0,0 +1,21 @@
package anti_spam
type CheckerName string
type CheckerParams struct {
BlogURL string
Content string
CommentID uint
UserName string
UserEmail string
UserID uint
UserIP string
UserAgent string
}
type Checker interface {
Name() string
Check(p *CheckerParams) (bool, error)
}
+109
View File
@@ -0,0 +1,109 @@
package anti_spam
import (
"fmt"
"os"
"strconv"
"strings"
"sync"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/utils"
)
var _ Checker = (*KeywordsChecker)(nil)
type KwCheckerMode int
const (
KwCheckerModeBlock KwCheckerMode = iota // 仅拦截
KwCheckerModeReplace // 仅替换关键词
)
type KeywordsCheckerConf struct {
Files []string
FileSep string
ReplaceTo string
Mode KwCheckerMode
OnUpdateComment func(commentID uint, content string) error
}
type KeywordsChecker struct {
conf *KeywordsCheckerConf
keywords *[]string
mux sync.Mutex
}
func NewKeywordsChecker(conf *KeywordsCheckerConf) *KeywordsChecker {
return &KeywordsChecker{
conf: conf,
}
}
func (*KeywordsChecker) Name() string {
return "keywords"
}
func (c *KeywordsChecker) Check(p *CheckerParams) (bool, error) {
c.loadKeywords()
isContains := false
content := p.Content
for _, keyword := range *c.keywords {
if strings.Contains(p.Content, keyword) {
isContains = true
if c.conf.Mode == KwCheckerModeReplace {
content = strings.Replace(content, keyword,
strings.Repeat(c.conf.ReplaceTo, len([]rune(keyword))), -1)
}
}
}
if c.conf.Mode == KwCheckerModeReplace {
if isContains {
log.Info(LOG_TAG, fmt.Sprintf("keyword replace comment id=%d original=%s processed=%s",
p.CommentID, strconv.Quote(p.Content), strconv.Quote(content)))
// 更新评论
if c.conf.OnUpdateComment != nil {
c.conf.OnUpdateComment(p.CommentID, content)
}
}
return true, nil
}
if c.conf.Mode == KwCheckerModeBlock {
return !isContains, nil
}
return true, nil
}
func (c *KeywordsChecker) loadKeywords() error {
c.mux.Lock()
defer c.mux.Unlock()
// 已加载过无需再次加载
if c.keywords != nil {
return nil
}
c.keywords = &[]string{}
// 加载文件
for _, f := range c.conf.Files {
buf, err := os.ReadFile(f)
if err != nil {
return fmt.Errorf("failed to load Keywords file: %s, %w", strconv.Quote(f), err)
}
fileContent := string(buf)
aKeywords := utils.SplitAndTrimSpace(fileContent, c.conf.FileSep)
*c.keywords = append(*c.keywords, aKeywords...)
}
return nil
}
+123
View File
@@ -0,0 +1,123 @@
package anti_spam
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/log"
)
func (as AntiSpam) getEnabledCheckers() []Checker {
checkers := []Checker{}
akismetKey := strings.TrimSpace(as.conf.AkismetKey)
if akismetKey != "" {
checkers = append(checkers, NewAkismetChecker(akismetKey))
}
// 腾讯云
tencentConf := as.conf.Tencent
if tencentConf.Enabled {
checkers = append(checkers, NewTencentChecker(
tencentConf.SecretID, tencentConf.SecretKey, tencentConf.Region))
}
// 阿里云
aliyunConf := as.conf.Aliyun
if aliyunConf.Enabled {
checkers = append(checkers, NewAliyunChecker(
aliyunConf.AccessKeyID, aliyunConf.AccessKeySecret, aliyunConf.Region))
}
// 关键字过滤
keywordsConf := as.conf.Keywords
if keywordsConf.Enabled {
var kwCheckerMode KwCheckerMode
if as.conf.Keywords.Pending {
kwCheckerMode = KwCheckerModeBlock
} else {
kwCheckerMode = KwCheckerModeReplace
}
checkers = append(checkers, NewKeywordsChecker(&KeywordsCheckerConf{
Files: as.conf.Keywords.Files,
FileSep: as.conf.Keywords.FileSep,
ReplaceTo: as.conf.Keywords.ReplacTo,
Mode: kwCheckerMode,
OnUpdateComment: func(commentID uint, content string) error {
comment := as.dao.FindComment(commentID)
comment.Content = content
as.dao.UpdateComment(&comment)
return nil
},
}))
}
return checkers
}
// 拦截评论
func (as AntiSpam) blockComment(checker Checker, comment *entity.Comment) {
log.Info(LOG_TAG, fmt.Sprintf("[%s] Successful blocking of comments ID=%d CONT=%s",
checker.Name(), comment.ID, strconv.Quote(comment.Content)))
if comment.IsPending {
return
}
comment.IsPending = true // 改为待审状态
as.dao.UpdateComment(comment)
}
func (as AntiSpam) getCheckerParams(data *CheckData) *CheckerParams {
user := as.dao.FetchUserForComment(data.Comment)
siteURL := ""
if data.Comment.SiteName != "" {
site := as.dao.FindSite(data.Comment.SiteName)
siteURL = as.dao.CookSite(&site).FirstUrl
}
if siteURL == "" { // 从 referer 中提取网站
if pr, err := url.Parse(data.ReqReferer); err == nil && pr.Scheme != "" && pr.Host != "" {
siteURL = fmt.Sprintf("%s://%s", pr.Scheme, pr.Host)
}
}
return &CheckerParams{
BlogURL: siteURL,
Content: data.Comment.Content,
CommentID: data.Comment.ID,
UserName: user.Name,
UserEmail: user.Email,
UserID: user.ID,
UserIP: data.ReqIP,
UserAgent: data.ReqUserAgent,
}
}
func (as AntiSpam) checkerTrigger(checker Checker, data *CheckData) {
params := as.getCheckerParams(data)
isPass, err := checker.Check(params)
if err != nil { // Api 发生错误
log.Error(LOG_TAG, fmt.Sprintf(
"%s Interception error occurred ID=%d Err:", checker.Name(), data.Comment.ID), err)
if as.conf.ApiFailBlock {
as.blockComment(checker, data.Comment) // 仍然拦截
}
return
}
if !isPass { // not Pass 且 Api 未发生错误
as.blockComment(checker, data.Comment) // 拦截评论
}
}
@@ -6,27 +6,34 @@ import (
"github.com/ArtalkJS/Artalk/internal/cloud/qcloud"
)
type TencentParams struct {
var _ Checker = (*TencentChecker)(nil)
type TencentChecker struct {
SecretID string
SecretKey string
Region string
}
Content string
CommentID uint
func NewTencentChecker(secretID, secretKey, region string) Checker {
return &TencentChecker{
SecretID: secretID,
SecretKey: secretKey,
Region: region,
}
}
UserName string
UserID uint
UserIP string
func (*TencentChecker) Name() string {
return "tencent"
}
// 腾讯云文本内容安全 TMS
// @link https://cloud.tencent.com/document/product/1124/51860
// @link https://console.cloud.tencent.com/cms/text/overview
func Tencent(p TencentParams) (isPass bool, err error) {
func (c *TencentChecker) Check(p *CheckerParams) (bool, error) {
return qcloud.TMS(qcloud.TmsConf{
SecretID: p.SecretID,
SecretKey: p.SecretKey,
Region: p.Region,
SecretID: c.SecretID,
SecretKey: c.SecretKey,
Region: c.Region,
Content: p.Content,
DataID: fmt.Sprintf("comment-%d", p.CommentID),
UserID: fmt.Sprintf("%d", p.UserID),
-269
View File
@@ -1,269 +0,0 @@
package artransfer
import (
"fmt"
"os"
"strconv"
"time"
"github.com/ArtalkJS/Artalk/internal/db"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/query"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/cheggaaa/pb/v3"
)
var ArtransImporter = &_ArtransImporter{
ImporterInfo: ImporterInfo{
Name: "artrans",
Desc: "Import from Artrans",
Note: "",
},
}
type _ArtransImporter struct {
ImporterInfo
}
func (imp *_ArtransImporter) Run(basic *BasicParams, payload []string) {
// 读取文件
jsonStr, jErr := JsonFileReady(payload)
if jErr != nil {
logFatal(jErr)
return
}
ImportArtransByStr(basic, jsonStr)
}
func ImportArtransByStr(basic *BasicParams, str string) {
// 解析内容
comments := []entity.Artran{}
dErr := JsonDecodeFAS(str, &comments)
if dErr != nil {
logFatal(dErr)
return
}
ImportArtrans(basic, comments)
}
func ImportArtrans(basic *BasicParams, srcComments []entity.Artran) {
if len(srcComments) == 0 {
logFatal(i18n.T("No comment"))
return
}
if basic.TargetSiteUrl != "" && !utils.ValidateURL(basic.TargetSiteUrl) {
logFatal(i18n.T("Invalid {{name}}", map[string]interface{}{"name": i18n.T("Target Site") + " " + "URL"}))
return
}
// 汇总
print("# " + i18n.T("Please review") + ":\n\n")
// 第一条评论
PrintEncodeData(i18n.T("First comment"), srcComments[0])
showTSiteName := basic.TargetSiteName
showTSiteUrl := basic.TargetSiteUrl
if showTSiteName == "" {
showTSiteName = i18n.T("Unspecified")
}
if showTSiteUrl == "" {
showTSiteUrl = i18n.T("Unspecified")
}
// 目标站点名和目标站点URL都不为空,才开启 URL 解析器
showUrlResolver := "off"
if basic.UrlResolver {
showUrlResolver = "on"
}
// if basic.TargetSiteName != "" && basic.TargetSiteUrl != "" {
// basic.UrlResolver = true
// showUrlResolver = "on"
// }
PrintTable([][]interface{}{
{i18n.T("Target Site") + " " + i18n.T("Name"), showTSiteName},
{i18n.T("Target Site") + " URL", showTSiteUrl},
{i18n.T("Comment count"), fmt.Sprintf("%d", len(srcComments))},
{i18n.T("URL Resolver"), showUrlResolver},
})
print("\n")
// 确认开始
if !Confirm(i18n.T("Confirm to continue?")) {
os.Exit(0)
}
// 准备导入评论
print("\n")
importComments := []entity.Comment{}
srcIdToIndexMap := map[string]uint{} // 源 ID 映射表 srcID => index
createdDates := map[int]time.Time{}
updatedDates := map[int]time.Time{}
// 解析 comments
for i, c := range srcComments {
srcIdToIndexMap[c.ID] = uint(i + 1) // 防 0 出没
}
for i, c := range srcComments {
siteName := c.SiteName
siteUrls := c.SiteUrls
if basic.TargetSiteName != "" {
siteName = basic.TargetSiteName
}
if basic.TargetSiteUrl != "" {
siteUrls = basic.TargetSiteUrl
}
// 准备 site
site, sErr := SiteReady(siteName, siteUrls)
if sErr != nil {
logFatal(sErr)
return
}
// 准备 user
user := query.FindCreateUser(c.Nick, c.Email, c.Link)
if !user.IsAdmin {
userModified := false
if c.BadgeName != "" && c.BadgeName != user.BadgeName {
user.BadgeName = c.BadgeName
userModified = true
}
if c.BadgeColor != "" && c.BadgeColor != user.BadgeColor {
user.BadgeColor = c.BadgeColor
userModified = true
}
if userModified {
query.UpdateUser(&user)
}
}
// 准备 page
nPageKey := c.PageKey
if basic.UrlResolver { // 使用 URL 解析器
splittedURLs := utils.SplitAndTrimSpace(basic.TargetSiteUrl, ",")
nPageKey = UrlResolverGetPageKey(splittedURLs[0], c.PageKey)
}
page := query.FindCreatePage(nPageKey, c.PageTitle, site.Name)
adminOnlyVal := c.PageAdminOnly == utils.ToString(true)
if page.AdminOnly != adminOnlyVal {
page.AdminOnly = adminOnlyVal
query.UpdatePage(&page)
}
voteUp, _ := strconv.Atoi(c.VoteUp)
voteDown, _ := strconv.Atoi(c.VoteDown)
// 创建新 comment 实例
nComment := entity.Comment{
Rid: srcIdToIndexMap[c.Rid], // [-1-] rid => index+1
Content: c.Content,
UA: c.UA,
IP: c.IP,
IsCollapsed: c.IsCollapsed == utils.ToString(true),
IsPending: c.IsPending == utils.ToString(true),
IsPinned: c.IsPinned == utils.ToString(true),
VoteUp: voteUp,
VoteDown: voteDown,
UserID: user.ID,
PageKey: page.Key,
SiteName: site.Name,
}
// 时间还原
createdDates[i] = ParseDate(c.CreatedAt)
if c.UpdatedAt != "" {
updatedDates[i] = ParseDate(c.UpdatedAt)
} else {
updatedDates[i] = ParseDate(c.CreatedAt)
}
importComments = append(importComments, nComment)
}
println(i18n.T("Saving") + "...")
// Batch Insert
// @link https://gorm.io/docs/create.html#Batch-Insert
db.DB().CreateInBatches(&importComments, 100)
// ID 变更映射表 index => new_db_id
indexToDbIdMap := map[uint]uint{}
for i, savedComment := range importComments {
indexToDbIdMap[uint(i+1)] = savedComment.ID
}
// 进度条
var bar *pb.ProgressBar
if HttpOutput == nil {
bar = pb.StartNew(len(srcComments))
}
total := len(srcComments)
for i, savedComment := range importComments {
// 日期恢复
// @see https://gorm.io/zh_CN/docs/conventions.html#CreatedAt
// @see https://github.com/go-gorm/gorm/issues/4827#issuecomment-960480148 无语...
// TODO
// savedComment.CreatedAt = createdDates[i] // 无效
// savedComment.UpdatedAt = updatedDates[i]
updateData := map[string]interface{}{
"CreatedAt": createdDates[i],
"UpdatedAt": updatedDates[i],
}
// Rid 重建
if savedComment.Rid != 0 {
updateData["Rid"] = indexToDbIdMap[savedComment.Rid] // [-2-] index+1 => db_new_id
}
db.DB().Model(&savedComment).Updates(updateData)
// Vote 重建 (伪投票)
if savedComment.VoteUp > 0 {
for i := 0; i < savedComment.VoteUp; i++ {
query.NewVote(savedComment.ID, entity.VoteTypeCommentUp, 0, "", "")
}
}
if savedComment.VoteDown > 0 {
for i := 0; i < savedComment.VoteDown; i++ {
query.NewVote(savedComment.ID, entity.VoteTypeCommentDown, 0, "", "")
}
}
if bar != nil {
bar.Increment()
}
if HttpOutput != nil && i%50 == 0 {
print(fmt.Sprintf("%.0f%%... ", float64(i)/float64(total)*100))
}
}
if bar != nil {
bar.Finish()
}
if HttpOutput != nil {
println()
}
logInfo(i18n.T("{{count}} items imported", map[string]interface{}{"count": len(srcComments)}))
}
+62
View File
@@ -0,0 +1,62 @@
package artransfer
import (
"github.com/ArtalkJS/Artalk/internal/dao"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
)
func RunExportArtrans(dao *dao.Dao, params *ExportParams) (string, error) {
return exportArtrans(dao, params)
}
func RunImportArtrans(dao *dao.Dao, params *ImportParams) {
if !params.UrlResolver {
logWarn("Target site URL resolver disabled")
}
// 读取 JSON
if params.JsonData == "" {
if params.JsonFile == "" {
logFatal(i18n.T("{{name}} is required", map[string]any{"name": "json_file:<JSON file path>"}))
return
}
var err error
params.JsonData, err = readJsonFile(params.JsonFile)
if err != nil {
logFatal(err)
return
}
}
// Json 转 Artran 实例列表
comments := []*entity.Artran{}
if err := jsonDecodeFAS(params.JsonData, &comments); err != nil {
logFatal(err)
return
}
// 执行导入数据
importArtrans(dao, params, comments)
print("\n")
logInfo(i18n.T("Import complete"))
}
func ArrToImportParams(arr []string) *ImportParams {
params := 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,
})
return &params
}
+53
View File
@@ -0,0 +1,53 @@
package artransfer
import (
"encoding/json"
"testing"
"github.com/ArtalkJS/Artalk/internal/dao"
"github.com/ArtalkJS/Artalk/internal/db"
"github.com/ArtalkJS/Artalk/test"
"github.com/stretchr/testify/assert"
)
func TestRunExportAndImportArtrans(t *testing.T) {
app, _ := test.NewTestApp()
defer app.Cleanup()
var artransJSON string
t.Run("Export", func(t *testing.T) {
var err error
artransJSON, err = RunExportArtrans(app.Dao(), &ExportParams{
SiteNameScope: []string{},
})
if err != nil {
t.Fatal(err)
}
})
assert.NotEmpty(t, artransJSON)
t.Run("Import", func(t *testing.T) {
ddb, _ := db.NewTestDB()
defer db.CloseDB(ddb)
dao := dao.NewDao(ddb)
RunImportArtrans(dao, &ImportParams{
JsonData: artransJSON,
Assumeyes: true,
})
// check equals
exported, err := RunExportArtrans(dao, &ExportParams{})
if assert.NoError(t, err) {
jsonArrCount := func(str string) int {
var arr []any
json.Unmarshal([]byte(str), &arr)
return len(arr)
}
assert.Equal(t, jsonArrCount(artransJSON), jsonArrCount(exported))
}
})
}
-16
View File
@@ -1,16 +0,0 @@
package artransfer
var ExampleImporter = &_ExampleImporter{
ImporterInfo: ImporterInfo{
Name: "example",
Desc: "Import data from <EXAMPLE>",
Note: "",
},
}
type _ExampleImporter struct {
ImporterInfo
}
func (imp *_ExampleImporter) Run(basic *BasicParams, payload []string) {
}
+14 -5
View File
@@ -3,19 +3,28 @@ package artransfer
import (
"encoding/json"
"github.com/ArtalkJS/Artalk/internal/db"
"github.com/ArtalkJS/Artalk/internal/dao"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/query"
"gorm.io/gorm"
)
func ExportArtransString(dbScopes ...func(*gorm.DB) *gorm.DB) (string, error) {
type ExportParams struct {
SiteNameScope []string `json:"site_name_scope"`
}
func exportArtrans(dao *dao.Dao, params *ExportParams) (string, error) {
comments := []entity.Comment{}
db.DB().Scopes(dbScopes...).Find(&comments)
dao.DB().Scopes(func(db *gorm.DB) *gorm.DB {
if len(params.SiteNameScope) > 0 {
db = db.Where("site_name IN (?)", params.SiteNameScope)
}
return db
}).Find(&comments)
artrans := []entity.Artran{}
for _, c := range comments {
ct := query.CommentToArtran(&c)
ct := dao.CommentToArtran(&c)
artrans = append(artrans, ct)
}
+223 -276
View File
@@ -1,121 +1,262 @@
package artransfer
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/dao"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/query"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/araddon/dateparse"
"github.com/cheggaaa/pb/v3"
)
func RunImportArtrans(payload []string) {
basic := GetBasicParamsFrom(payload)
name := ArtransImporter.ImporterInfo.Name
desc := ArtransImporter.ImporterInfo.Desc
note := ArtransImporter.ImporterInfo.Note
print("\n")
tableData := [][]interface{}{
{"Artransfer - Import"},
{strings.ToUpper(name)},
{desc},
}
if note != "" {
tableData = append(tableData, []interface{}{note})
}
PrintTable(tableData)
print("\n")
//t1 := time.Now()
ArtransImporter.Run(basic, payload)
//elapsed := time.Since(t1)
print("\n")
logInfo(i18n.T("Import complete")) //,耗时: ", elapsed)
}
type ImporterInfo struct {
Name string
Desc string
Note string
}
func GetImporterInfo(instance interface{}) ImporterInfo {
var info ImporterInfo
j, _ := json.Marshal(instance)
json.Unmarshal(j, &info)
return info
}
type BasicParams struct {
type ImportParams struct {
TargetSiteName string
TargetSiteUrl string
UrlResolver bool
UrlResolver bool
JsonFile string
JsonData string
Assumeyes bool // Automatically answer yes for all questions.
}
func GetBasicParamsFrom(payload []string) *BasicParams {
basic := BasicParams{}
func importArtrans(dao *dao.Dao, params *ImportParams, comments []*entity.Artran) {
if len(comments) == 0 {
logFatal(i18n.T("No comment"))
return
}
basic.UrlResolver = false // 默认关闭
if params.TargetSiteUrl != "" && !utils.ValidateURL(params.TargetSiteUrl) {
logFatal(i18n.T("Invalid {{name}}", map[string]interface{}{"name": i18n.T("Target Site") + " " + "URL"}))
return
}
GetParamsFrom(payload).To(map[string]interface{}{
"t_name": &basic.TargetSiteName,
"t_url": &basic.TargetSiteUrl,
"t_url_resolver": &basic.UrlResolver,
// 汇总
print("# " + i18n.T("Please review") + ":\n\n")
// 第一条评论
printEncodeData(i18n.T("First comment"), comments[0])
showTSiteName := params.TargetSiteName
showTSiteUrl := params.TargetSiteUrl
if showTSiteName == "" {
showTSiteName = i18n.T("Unspecified")
}
if showTSiteUrl == "" {
showTSiteUrl = i18n.T("Unspecified")
}
// 目标站点名和目标站点URL都不为空,才开启 URL 解析器
showUrlResolver := "off"
if params.UrlResolver {
showUrlResolver = "on"
}
// if basic.TargetSiteName != "" && basic.TargetSiteUrl != "" {
// basic.UrlResolver = true
// showUrlResolver = "on"
// }
printTable([][]interface{}{
{i18n.T("Target Site") + " " + i18n.T("Name"), showTSiteName},
{i18n.T("Target Site") + " URL", showTSiteUrl},
{i18n.T("Comment count"), fmt.Sprintf("%d", len(comments))},
{i18n.T("URL Resolver"), showUrlResolver},
})
if !basic.UrlResolver {
logWarn("Target site URL resolver disabled")
print("\n")
// 确认开始
if !params.Assumeyes && !confirm(i18n.T("Confirm to continue?")) {
os.Exit(0)
}
return &basic
}
// 准备导入评论
print("\n")
func RequiredBasicTargetSite(basic *BasicParams) error {
if basic.TargetSiteName == "" {
return errors.New(i18n.T("{{name}} is required", map[string]interface{}{"name": "t_name:<Target Site Name>"}))
}
if basic.TargetSiteUrl == "" {
return errors.New(i18n.T("{{name}} is required", map[string]interface{}{"name": "t_url:<Target Site Root URL>"}))
}
if !utils.ValidateURL(basic.TargetSiteUrl) {
return errors.New("invalid URL for parameter `t_url:<Target Site Root URL>`")
importComments := []*entity.Comment{}
srcIdToIndexMap := map[string]uint{} // 源 ID 映射表 srcID => index
createdDates := map[int]time.Time{}
updatedDates := map[int]time.Time{}
// 解析 comments
for i, c := range comments {
srcIdToIndexMap[c.ID] = uint(i + 1) // 防 0 出没
}
return nil
for i, c := range comments {
siteName := c.SiteName
siteUrls := c.SiteUrls
if params.TargetSiteName != "" {
siteName = params.TargetSiteName
}
if params.TargetSiteUrl != "" {
siteUrls = params.TargetSiteUrl
}
// 准备 site
site, sErr := siteReady(dao, siteName, siteUrls)
if sErr != nil {
logFatal(sErr)
return
}
// 准备 user
user := dao.FindCreateUser(c.Nick, c.Email, c.Link)
if !user.IsAdmin {
userModified := false
if c.BadgeName != "" && c.BadgeName != user.BadgeName {
user.BadgeName = c.BadgeName
userModified = true
}
if c.BadgeColor != "" && c.BadgeColor != user.BadgeColor {
user.BadgeColor = c.BadgeColor
userModified = true
}
if userModified {
dao.UpdateUser(&user)
}
}
// 准备 page
nPageKey := c.PageKey
if params.UrlResolver { // 使用 URL 解析器
splittedURLs := utils.SplitAndTrimSpace(params.TargetSiteUrl, ",")
nPageKey = urlResolverGetPageKey(splittedURLs[0], c.PageKey)
}
page := dao.FindCreatePage(nPageKey, c.PageTitle, site.Name)
adminOnlyVal := c.PageAdminOnly == utils.ToString(true)
if page.AdminOnly != adminOnlyVal {
page.AdminOnly = adminOnlyVal
dao.UpdatePage(&page)
}
voteUp, _ := strconv.Atoi(c.VoteUp)
voteDown, _ := strconv.Atoi(c.VoteDown)
// 创建新 comment 实例
nComment := entity.Comment{
Rid: srcIdToIndexMap[c.Rid], // [-1-] rid => index+1
Content: c.Content,
UA: c.UA,
IP: c.IP,
IsCollapsed: c.IsCollapsed == utils.ToString(true),
IsPending: c.IsPending == utils.ToString(true),
IsPinned: c.IsPinned == utils.ToString(true),
VoteUp: voteUp,
VoteDown: voteDown,
UserID: user.ID,
PageKey: page.Key,
SiteName: site.Name,
}
// 时间还原
createdDates[i] = parseDate(c.CreatedAt)
if c.UpdatedAt != "" {
updatedDates[i] = parseDate(c.UpdatedAt)
} else {
updatedDates[i] = parseDate(c.CreatedAt)
}
importComments = append(importComments, &nComment)
}
println(i18n.T("Saving") + "...")
// Batch Insert
// @link https://gorm.io/docs/create.html#Batch-Insert
dao.DB().CreateInBatches(&importComments, 100)
// ID 变更映射表 index => new_db_id
indexToDbIdMap := map[uint]uint{}
for i, savedComment := range importComments {
indexToDbIdMap[uint(i+1)] = savedComment.ID
}
// 进度条
var bar *pb.ProgressBar
if HttpOutput == nil {
bar = pb.StartNew(len(comments))
}
total := len(comments)
for i, savedComment := range importComments {
// 日期恢复
// @see https://gorm.io/zh_CN/docs/conventions.html#CreatedAt
// @see https://github.com/go-gorm/gorm/issues/4827#issuecomment-960480148 无语...
// TODO
// savedComment.CreatedAt = createdDates[i] // 无效
// savedComment.UpdatedAt = updatedDates[i]
updateData := map[string]interface{}{
"CreatedAt": createdDates[i],
"UpdatedAt": updatedDates[i],
}
// Rid 重建
if savedComment.Rid != 0 {
updateData["Rid"] = indexToDbIdMap[savedComment.Rid] // [-2-] index+1 => db_new_id
}
dao.DB().Model(&savedComment).Updates(updateData)
// Vote 重建 (伪投票)
if savedComment.VoteUp > 0 {
for i := 0; i < savedComment.VoteUp; i++ {
dao.NewVote(savedComment.ID, entity.VoteTypeCommentUp, 0, "", "")
}
}
if savedComment.VoteDown > 0 {
for i := 0; i < savedComment.VoteDown; i++ {
dao.NewVote(savedComment.ID, entity.VoteTypeCommentDown, 0, "", "")
}
}
if bar != nil {
bar.Increment()
}
if HttpOutput != nil && i%50 == 0 {
print(fmt.Sprintf("%.0f%%... ", float64(i)/float64(total)*100))
}
}
if bar != nil {
bar.Finish()
}
if HttpOutput != nil {
println()
}
logInfo(i18n.T("{{count}} items imported", map[string]interface{}{"count": len(comments)}))
}
// 站点准备
func SiteReady(tSiteName string, tSiteUrls string) (entity.Site, error) {
site := query.FindSite(tSiteName)
func siteReady(dao *dao.Dao, tSiteName string, tSiteUrls string) (*entity.Site, error) {
site := dao.FindSite(tSiteName)
if site.IsEmpty() {
// 创建新站点
site = entity.Site{}
site.Name = tSiteName
site.Urls = tSiteUrls
err := query.CreateSite(&site)
err := dao.CreateSite(&site)
if err != nil {
return entity.Site{}, errors.New("failed to create site")
return nil, fmt.Errorf("failed to create site")
}
} else {
// 追加 URL
siteCooked := query.CookSite(&site)
siteCooked := dao.CookSite(&site)
urlExist := func(tUrl string) bool {
for _, u := range siteCooked.Urls {
@@ -139,206 +280,12 @@ func SiteReady(tSiteName string, tSiteUrls string) (entity.Site, error) {
// 保存
rUrls = append(rUrls, siteCooked.Urls...)
site.Urls = strings.Join(rUrls, ",")
err := query.UpdateSite(&site)
err := dao.UpdateSite(&site)
if err != nil {
return entity.Site{}, errors.New("update site data failed")
return nil, fmt.Errorf("update site data failed")
}
}
}
return site, nil
}
func JsonFileReady(payload []string) (string, error) {
var jsonFile, jsonData string
GetParamsFrom(payload).To(map[string]interface{}{
"json_file": &jsonFile,
"json_data": &jsonData,
})
// 直接给 JSON 内容,不去读取文件
if jsonData != "" {
return jsonData, nil
}
if jsonFile == "" {
return "", errors.New(i18n.T("{{name}} is required", map[string]interface{}{"name": "json_file:<JSON file path>"}))
}
if _, err := os.Stat(jsonFile); errors.Is(err, os.ErrNotExist) {
return "", errors.New(i18n.T("{{name}} not found", map[string]interface{}{"name": i18n.T("File")}))
}
buf, err := os.ReadFile(jsonFile)
if err != nil {
return "", errors.New("file open failed" + ": " + err.Error())
}
return string(buf), nil
}
// PageKey (commentUrlVal 不确定是否为完整 URL 还是一个 path)
//
// @examples
// ("https://github.com", "/1.html") => "https://github.com/1.html"
// ("https://github.com", "https://xxx.com/1.html") => "https://github.com/1.html"
// ("https://github.com/", "/1.html") => "https://github.com/1.html"
// ("", "/1.html") => "/1.html"
// ("", "https://xxx.com/1.html") => "https://xxx.com/1.html"
// ("https://github.com/233", "/1/") => "https://github.com/1/"
func UrlResolverGetPageKey(baseUrlRaw string, commentUrlRaw string) string {
if baseUrlRaw == "" {
return commentUrlRaw
}
baseUrl, err := url.Parse(baseUrlRaw)
if err != nil {
return commentUrlRaw
}
commentUrl, err := url.Parse(commentUrlRaw)
if err != nil {
return commentUrlRaw
}
// "https://artalk.js.org/guide/describe.html?233" => "/guide/describe.html?233"
commentUrl.Scheme = ""
commentUrl.Host = ""
// 解决拼接路径中的相对地址,例如:https://atk.xxx/abc/../artalk => https://atk.xxx/artalk
url := baseUrl.ResolveReference(commentUrl)
return url.String()
}
func ParseDate(s string) time.Time {
denverLoc, _ := time.LoadLocation(config.Instance.TimeZone) // 时区
time.Local = denverLoc
// TODO should be restricted to using only the RFC3339 standard time format
t, _ := dateparse.ParseIn(s, denverLoc)
return t
}
type _getParamsTo struct {
To func(variables map[string]interface{})
}
func GetParamsFrom(payload []string) _getParamsTo {
a := _getParamsTo{}
a.To = func(variables map[string]interface{}) {
for _, pVal := range payload {
for fromName, toVar := range variables {
if !strings.HasPrefix(pVal, fromName+":") {
continue
}
valStr := strings.TrimPrefix(pVal, fromName+":")
switch reflect.ValueOf(toVar).Interface().(type) {
case *string:
*toVar.(*string) = valStr
case *bool:
*toVar.(*bool) = strings.EqualFold(valStr, "true")
case *int:
num, err := strconv.Atoi(valStr)
if err != nil {
*toVar.(*int) = num
}
}
break
}
}
}
return a
}
func GetArrayParamsFrom(payload []string, key string) []string {
arr := []string{}
for _, pVal := range payload {
if strings.HasPrefix(pVal, key+":") {
arr = append(arr, strings.TrimPrefix(pVal, key+":"))
}
}
return arr
}
func CheckIfJsonArr(str string) bool {
x := bytes.TrimSpace([]byte(str))
return len(x) > 0 && x[0] == '['
}
func CheckIfJsonObj(str string) bool {
x := bytes.TrimSpace([]byte(str))
return len(x) > 0 && x[0] == '{'
}
func TryConvertLineJsonToArr(str string) (string, error) {
// 尝试将一行一行的 Obj 转成 Arr
arrTmp := []map[string]interface{}{}
for _, line := range strings.Split(strings.TrimSpace(str), "\n") {
var tmp map[string]interface{}
err := json.Unmarshal([]byte(line), &tmp)
if err != nil {
return "", err
}
arrTmp = append(arrTmp, tmp)
}
r, err := json.Marshal(arrTmp)
if err != nil {
return "", err
}
return string(r), nil
}
// Json Decode (FAS: Fields All String Type)
// 解析 json 为字段全部是 string 类型的 struct
func JsonDecodeFAS(str string, fasStructure interface{}) error {
if !CheckIfJsonArr(str) {
var err error
str, err = TryConvertLineJsonToArr(str)
if err != nil {
return errors.New("JSON of array type is required: " + err.Error())
}
}
err := json.Unmarshal([]byte(utils.JsonObjInArrAnyStr(str)), fasStructure) // lib.ToString()
if err != nil {
return errors.New("failed to parse JSON: " + err.Error())
}
return nil
}
func HideJsonLongText(key string, text string) string {
r := regexp.MustCompile(key + `:"(.+?)"`)
sm := r.FindStringSubmatch(text)
postText := ""
if len(sm) > 0 {
postText = sm[1]
}
text = r.ReplaceAllString(text, fmt.Sprintf(key+": <!-- 省略 %d 个字符 -->", utf8.RuneCountInString(postText)))
return text
}
// 传入 ID 变更表 (原始ID => 数据库已存在记录的ID) rid 将根据此替换
func RebuildRid(idChanges map[uint]uint) {
for _, newId := range idChanges {
nComment := query.FindComment(newId)
if nComment.Rid == 0 {
continue
}
if newId, isExist := idChanges[nComment.Rid]; isExist {
nComment.Rid = newId
err := query.UpdateComment(&nComment)
if err != nil {
logError(fmt.Sprintf("[rid 更新] new_id:%d new_rid:%d", nComment.ID, newId), err)
}
}
}
print("\n")
logInfo("RID 重构完毕")
return &site, nil
}
+57
View File
@@ -0,0 +1,57 @@
package artransfer
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"github.com/ArtalkJS/Artalk/internal/utils"
)
func checkIfJsonArr(str string) bool {
x := bytes.TrimSpace([]byte(str))
return len(x) > 0 && x[0] == '['
}
func checkIfJsonObj(str string) bool {
x := bytes.TrimSpace([]byte(str))
return len(x) > 0 && x[0] == '{'
}
func tryConvertLineJsonToArr(str string) (string, error) {
// 尝试将一行一行的 Obj 转成 Arr
arrTmp := []map[string]interface{}{}
for _, line := range strings.Split(strings.TrimSpace(str), "\n") {
var tmp map[string]interface{}
err := json.Unmarshal([]byte(line), &tmp)
if err != nil {
return "", err
}
arrTmp = append(arrTmp, tmp)
}
r, err := json.Marshal(arrTmp)
if err != nil {
return "", err
}
return string(r), nil
}
// Json Decode (FAS: Fields All String Type)
// 解析 json 为字段全部是 string 类型的 struct
func jsonDecodeFAS(str string, fasStructure interface{}) error {
if !checkIfJsonArr(str) {
var err error
str, err = tryConvertLineJsonToArr(str)
if err != nil {
return fmt.Errorf("JSON of array type is required: %w", err)
}
}
err := json.Unmarshal([]byte(utils.JsonObjInArrAnyStr(str)), fasStructure) // lib.ToString()
if err != nil {
return fmt.Errorf("failed to parse JSON: %w", err)
}
return nil
}
@@ -3,17 +3,16 @@ package artransfer
import (
"bufio"
"fmt"
"log"
"os"
"strings"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/sirupsen/logrus"
)
// package-level 全局变量
// TODO 只支持单线执行,同时请求两个地方会出问题
var HttpOutput func(continueRun bool, text string)
var Assumeyes bool = false
func logError(a ...interface{}) {
if HttpOutput != nil {
@@ -21,7 +20,7 @@ func logError(a ...interface{}) {
return
}
logrus.Error(a...)
log.Error(a...)
}
func logFatal(a ...interface{}) {
@@ -30,7 +29,7 @@ func logFatal(a ...interface{}) {
return
}
logrus.Fatal(a...)
log.Fatal(a...)
}
func logWarn(a ...interface{}) {
@@ -39,7 +38,7 @@ func logWarn(a ...interface{}) {
return
}
logrus.Warn(a...)
log.Warn(a...)
}
func logInfo(a ...interface{}) {
@@ -48,7 +47,7 @@ func logInfo(a ...interface{}) {
return
}
logrus.Info(a...)
log.Info(a...)
}
func print(a ...interface{}) {
@@ -68,7 +67,7 @@ func println(a ...interface{}) {
print(fmt.Sprintln(a...))
}
func PrintTable(rows [][]interface{}) {
func printTable(rows [][]interface{}) {
if HttpOutput != nil {
println("-------------------------")
for _, row := range rows {
@@ -99,20 +98,15 @@ func PrintTable(rows [][]interface{}) {
println(t.Render())
}
func PrintEncodeData(dataType string, val interface{}) {
print(SprintEncodeData(dataType, val))
func printEncodeData(dataType string, val interface{}) {
print(sprintEncodeData(dataType, val))
}
func SprintEncodeData(dataType string, val interface{}) string {
func sprintEncodeData(dataType string, val interface{}) string {
return fmt.Sprintf("[%s]\n\n %#v\n\n", dataType, val)
}
func Confirm(s string) bool {
if Assumeyes {
printf("%s [y/n]: y", s)
return true
}
func confirm(s string) bool {
r := bufio.NewReader(os.Stdin)
for {
+37
View File
@@ -0,0 +1,37 @@
package artransfer
import "net/url"
// PageKey (commentUrlVal 不确定是否为完整 URL 还是一个 path)
//
// @examples
// ("https://github.com", "/1.html") => "https://github.com/1.html"
// ("https://github.com", "https://xxx.com/1.html") => "https://github.com/1.html"
// ("https://github.com/", "/1.html") => "https://github.com/1.html"
// ("", "/1.html") => "/1.html"
// ("", "https://xxx.com/1.html") => "https://xxx.com/1.html"
// ("https://github.com/233", "/1/") => "https://github.com/1/"
func urlResolverGetPageKey(baseUrlRaw string, commentUrlRaw string) string {
if baseUrlRaw == "" {
return commentUrlRaw
}
baseUrl, err := url.Parse(baseUrlRaw)
if err != nil {
return commentUrlRaw
}
commentUrl, err := url.Parse(commentUrlRaw)
if err != nil {
return commentUrlRaw
}
// "https://artalk.js.org/guide/describe.html?233" => "/guide/describe.html?233"
commentUrl.Scheme = ""
commentUrl.Host = ""
// 解决拼接路径中的相对地址,例如:https://atk.xxx/abc/../artalk => https://atk.xxx/artalk
url := baseUrl.ResolveReference(commentUrl)
return url.String()
}
+81
View File
@@ -0,0 +1,81 @@
package artransfer
import (
"errors"
"fmt"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/araddon/dateparse"
)
func readJsonFile(filename string) (string, error) {
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf(i18n.T("{{name}} not found", map[string]any{"name": i18n.T("File")}))
}
buf, err := os.ReadFile(filename)
if err != nil {
return "", fmt.Errorf("file open failed" + ": " + err.Error())
}
return string(buf), nil
}
func hideJsonLongText(key string, text string) string {
r := regexp.MustCompile(key + `:"(.+?)"`)
sm := r.FindStringSubmatch(text)
postText := ""
if len(sm) > 0 {
postText = sm[1]
}
text = r.ReplaceAllString(text, fmt.Sprintf(key+": <!-- 省略 %d 个字符 -->", utf8.RuneCountInString(postText)))
return text
}
func parseDate(s string) time.Time {
// TODO should be restricted to using only the RFC3339 standard time format
t, _ := dateparse.ParseIn(s, time.Local)
return t
}
type getParamsFromTo struct {
To func(variables map[string]any)
}
func getParamsFrom(arr []string) getParamsFromTo {
a := getParamsFromTo{}
a.To = func(variables map[string]any) {
for _, pVal := range arr {
for fromName, toVar := range variables {
if !strings.HasPrefix(pVal, fromName+":") {
continue
}
valStr := strings.TrimPrefix(pVal, fromName+":")
switch reflect.ValueOf(toVar).Interface().(type) {
case *string:
*toVar.(*string) = valStr
case *bool:
*toVar.(*bool) = strings.EqualFold(valStr, "true")
case *int:
num, err := strconv.Atoi(valStr)
if err != nil {
*toVar.(*int) = num
}
}
break
}
}
}
return a
}
+90
View File
@@ -0,0 +1,90 @@
package cache
import (
"reflect"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/eko/gocache/lib/v4/store"
"golang.org/x/sync/singleflight"
)
var (
cacheFindStoreGroup = new(singleflight.Group)
)
func (c *Cache) QueryDBWithCache(name string, dest any, queryDB func()) error {
if reflect.TypeOf(dest).Kind() != reflect.Ptr {
panic("The 'dest' param in 'QueryDBWithCache' func is expected to pointer type to update its data.")
}
// use SingleFlight to prevent Cache Breakdown
v, err, _ := cacheFindStoreGroup.Do(name, func() (any, error) {
// query cache
err := c.FindCache(name, dest)
if err != nil { // cache miss
// call queryDB() the dest value will be updated
queryDB()
if err := c.StoreCache(dest, name); err != nil {
return nil, err
}
// because queryDB() had update dest value,
// no need to update it again, so return nil
return nil, nil
}
return dest, nil
})
if err != nil {
return err
}
// update dest value only if cache hit,
// if cache miss, `dest` has been updated in `queryDB()` so no need to set again
if v != nil {
reflect.ValueOf(dest).Elem().Set(reflect.ValueOf(v).Elem()) // similar to `*dest = &v`
}
return nil
}
func (c *Cache) FindCache(name string, dest any) error {
// `Get()` is Thread Safe, so no need to add Mutex
// @see https://github.com/go-redis/redis/issues/23
_, err := c.marshal.Get(c.ctx, name, dest)
if err != nil {
log.Debug("[CacheMis] " + name)
return err
}
log.Debug("[CacheHit] " + name)
return nil
}
func (c *Cache) StoreCache(source any, name ...string) (err error) {
for _, n := range name {
log.Debug("[StoreCache] " + n)
// `Set()` is Thread Safe, no need to add Mutex
if setErr := c.marshal.Set(c.ctx, n, source,
store.WithExpiration(c.ttl),
); setErr != nil {
err = setErr
}
}
return
}
func (c *Cache) DelCache(name ...string) (err error) {
for _, n := range name {
log.Debug("[DelCache] " + n)
if delErr := c.marshal.Delete(c.ctx, n); delErr != nil {
err = delErr
}
}
return
}
+147
View File
@@ -0,0 +1,147 @@
package cache_test
import (
"fmt"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAction(t *testing.T) {
cache := newCache(t)
defer cache.Close()
doCrudTest := func(t *testing.T, testKey string, testData any) {
// Find Miss
{
var data any
err := cache.FindCache(testKey, &data)
assert.Error(t, err, "a find cache miss err should be occurred before store")
assert.Zero(t, data, "cache should not be hit before store it")
}
// Store
storeErr := cache.StoreCache(testData, testKey)
if assert.NoError(t, storeErr, "error occurred while cache store") {
// Find Hit
var data any
findErr := cache.FindCache(testKey, &data)
if assert.NoError(t, findErr, "cache data should be hit") {
assert.Equal(t, testData, data, "cache should equal to original data while cache hit")
}
}
// Del
delErr := cache.DelCache(testKey)
if assert.NoError(t, delErr, "error occurred while cache delete") {
// Find again
var dataAgain any
findErr := cache.FindCache(testKey, &dataAgain)
assert.Error(t, findErr, "a find cache miss err should be occurred after the cache had been deleted")
assert.Zero(t, dataAgain, "cache should not be found while cache deleted")
}
}
type testCase struct {
key string
value any
}
doTest := func(testCases []testCase) {
for _, tt := range testCases {
doCrudTest(t, tt.key, tt.value)
}
}
doTest([]testCase{
{key: "String", value: "12345"},
{key: "Integer", value: int64(114514)},
{key: "Float", value: float64(2.3333)},
})
t.Run("Concurrency", func(t *testing.T) {
const numRoutines = 100
var wg sync.WaitGroup
wg.Add(numRoutines)
for i := 0; i < numRoutines; i++ {
go func(num int) {
defer wg.Done()
doTest([]testCase{
{key: fmt.Sprintf("StringConcurrency_%d", num), value: "12345"},
{key: fmt.Sprintf("IntegerConcurrency_%d", num), value: int64(114514)},
{key: fmt.Sprintf("FloatConcurrency_%d", num), value: float64(2.3333)},
})
}(i)
}
wg.Wait()
})
}
func TestQueryDBWithCache(t *testing.T) {
cache := newCache(t)
defer cache.Close()
type user struct {
Name string
Email string
}
const key = "data_key_233"
var value = user{
Name: "qwqcode",
Email: "artalkjs@example.com",
}
doCachedFind := func() bool {
var data user
dbQueried := false
err := cache.QueryDBWithCache(key, &data, func() {
// simulate db query result
data = user{
Name: value.Name,
Email: value.Email,
}
dbQueried = true
})
if assert.NoError(t, err) {
assert.Equal(t, value, data)
}
return dbQueried
}
if dbQueried := doCachedFind(); dbQueried {
assert.True(t, dbQueried, "first call `QueryDBWithCache`, db should be queried")
}
if dbQueried := doCachedFind(); dbQueried {
assert.False(t, dbQueried, "second call `QueryDBWithCache`, db should not be queried")
}
t.Run("Concurrency", func(t *testing.T) {
const numRoutines = 100
var wg sync.WaitGroup
wg.Add(numRoutines)
for i := 0; i < numRoutines; i++ {
go func() {
defer wg.Done()
dbQueried := doCachedFind()
assert.False(t, dbQueried)
}()
}
wg.Wait()
})
}
-209
View File
@@ -1,209 +0,0 @@
package cache
import (
"errors"
"fmt"
"reflect"
"strings"
"time"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/eko/gocache/lib/v4/store"
"github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"
)
var (
CacheFindGroup = new(singleflight.Group)
)
func FindAndStoreCache(name string, dest interface{}, queryDBResult func() interface{}) error {
// SingleFlight 防止缓存击穿 (Cache breakdown)
v, err, _ := CacheFindGroup.Do(name, func() (interface{}, error) {
err := FindCache(name, dest)
// cache hit 直接返回结果
if err == nil {
return dest, nil
}
// cache miss 查数据库
result := queryDBResult()
if err := StoreCache(name, result); err != nil {
return nil, err
}
return result, nil
})
if err != nil {
return err
}
if v != nil {
reflect.ValueOf(dest).Elem().Set(reflect.ValueOf(v).Elem()) // similar to `*dest = &v`
}
return nil
}
func FindCache(name string, dest interface{}) error {
if !config.Instance.Cache.Enabled {
return errors.New("cache disabled")
}
// `Get()` is Thread Safe, so no need to add Mutex
// @see https://github.com/go-redis/redis/issues/23
_, err := CACHE.Get(Ctx, name, dest)
if err != nil {
return err
}
logrus.Debug("[Cache Hit] " + name)
return nil
}
func StoreCache(name string, source interface{}) error {
if !config.Instance.Cache.Enabled {
return nil
}
// `Set()` is Thread Safe too, no need to add Mutex either
err := CACHE.Set(Ctx, name, source,
store.WithExpiration(time.Duration(config.Instance.Cache.GetExpiresTime())),
)
if err != nil {
return err
}
logrus.Debug("[写入缓存] " + name)
return nil
}
func DelCache(name string) error {
if !config.Instance.Cache.Enabled {
return nil
}
return CACHE.Delete(Ctx, name)
}
func UserCacheSave(user *entity.User) error {
// 缓存 ID
err := StoreCache(fmt.Sprintf("user#id=%d", user.ID), user)
if err != nil {
return err
}
// 缓存 Name x Email
err = StoreCache(fmt.Sprintf("user#name=%s;email=%s", strings.ToLower(user.Name), strings.ToLower(user.Email)), user)
if err != nil {
return err
}
return err
}
func UserCacheDel(user *entity.User) {
DelCache(fmt.Sprintf("user#id=%d", user.ID))
DelCache(fmt.Sprintf("user#name=%s;email=%s", strings.ToLower(user.Name), strings.ToLower(user.Email)))
}
func SiteCacheSave(site *entity.Site) error {
// 缓存 ID
err := StoreCache(fmt.Sprintf("site#id=%d", site.ID), site)
if err != nil {
return err
}
// 缓存 Name
err = StoreCache(fmt.Sprintf("site#name=%s", site.Name), site)
if err != nil {
return err
}
return err
}
func SiteCacheDel(site *entity.Site) {
DelCache(fmt.Sprintf("site#id=%d", site.ID))
DelCache(fmt.Sprintf("site#name=%s", site.Name))
}
func PageCacheSave(page *entity.Page) error {
// 缓存 ID
err := StoreCache(fmt.Sprintf("page#id=%d", page.ID), page)
if err != nil {
return err
}
// 缓存 Key x SiteName
err = StoreCache(fmt.Sprintf("page#key=%s;site_name=%s", page.Key, page.SiteName), page)
if err != nil {
return err
}
return err
}
func PageCacheDel(page *entity.Page) {
DelCache(fmt.Sprintf("page#id=%d", page.ID))
DelCache(fmt.Sprintf("page#key=%s;site_name=%s", page.Key, page.SiteName))
}
func CommentCacheSave(comment *entity.Comment) error {
// 缓存 ID
err := StoreCache(fmt.Sprintf("comment#id=%d", comment.ID), comment)
if err != nil {
return err
}
// 缓存 Rid
if comment.Rid != 0 {
ChildCommentCacheSave(comment.Rid, comment.ID)
}
return err
}
func CommentCacheDel(comment *entity.Comment) {
DelCache(fmt.Sprintf("comment#id=%d", comment.ID))
// 清除 Rid 缓存
ChildCommentCacheDel(comment.ID)
if comment.Rid != 0 {
ChildCommentCacheDel(comment.Rid)
}
}
// 缓存 父ID=>子ID 评论数据
func ChildCommentCacheSave(parentID uint, childID uint) {
var childIDs []uint
var cacheName = fmt.Sprintf("parent-comments#pid=%d", parentID)
err := FindCache(cacheName, &childIDs)
if err != nil { // 初始化
childIDs = []uint{}
}
isExist := false
for _, i := range childIDs {
if i == childID {
isExist = true
break
}
}
// append if Not exist
if !isExist {
childIDs = append(childIDs, childID)
}
StoreCache(cacheName, childIDs)
}
func ChildCommentCacheDel(parentID uint) {
DelCache(fmt.Sprintf("parent-comments#pid=%d", parentID))
}
+40 -25
View File
@@ -2,82 +2,97 @@ package cache
import (
"context"
"log"
"strings"
"time"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/allegro/bigcache/v3"
"github.com/bradfitz/gomemcache/memcache"
"github.com/eko/gocache/lib/v4/cache"
lib_cache "github.com/eko/gocache/lib/v4/cache"
"github.com/eko/gocache/lib/v4/marshaler"
"github.com/eko/gocache/lib/v4/store"
bigcache_store "github.com/eko/gocache/store/bigcache/v4"
memcache_store "github.com/eko/gocache/store/memcache/v4"
redis_store "github.com/eko/gocache/store/redis/v4"
"github.com/go-redis/redis/v8"
"github.com/sirupsen/logrus"
"github.com/redis/go-redis/v9"
)
var (
CACHE *marshaler.Marshaler
)
type Cache struct {
ttl time.Duration
ctx context.Context
cancel context.CancelFunc
instance *lib_cache.Cache[any]
marshal *marshaler.Marshaler
}
var Ctx = context.Background()
func (cache *Cache) Close() {
cache.cancel()
cache.marshal = nil
cache.instance = nil
}
func OpenCache() (err error) {
cacheType := config.Instance.Cache.Type
func New(conf config.CacheConf) (*Cache, error) {
// create new context
ctx, cancel := context.WithCancel(context.Background())
cache := &Cache{
ttl: time.Duration(conf.GetExpiresTime()),
ctx: ctx,
cancel: cancel,
}
var cacheStore store.StoreInterface
switch cacheType {
switch conf.Type {
case config.CacheTypeBuiltin:
// 内建缓存
bigcacheClient, err := bigcache.NewBigCache(bigcache.DefaultConfig(
bigcacheClient, err := bigcache.New(context.Background(), bigcache.DefaultConfig(
// Tip: 内建缓存过期时间是一样的,只有 Redis/Memcache 才能设置单个 item 的
time.Duration(config.Instance.Cache.GetExpiresTime()),
cache.ttl,
))
if err != nil {
return err
return nil, err
}
cacheStore = bigcache_store.NewBigcache(bigcacheClient) // No options provided (as second argument)
case config.CacheTypeRedis:
// Redis
network := "tcp"
if config.Instance.Cache.Redis.Network != "" {
network = config.Instance.Cache.Redis.Network
if conf.Redis.Network != "" {
network = conf.Redis.Network
}
cacheStore = redis_store.NewRedis(redis.NewClient(&redis.Options{
Network: network,
Addr: config.Instance.Cache.Server,
Username: config.Instance.Cache.Redis.Username,
Password: config.Instance.Cache.Redis.Password,
DB: config.Instance.Cache.Redis.DB,
Addr: conf.Server,
Username: conf.Redis.Username,
Password: conf.Redis.Password,
DB: conf.Redis.DB,
}))
case config.CacheTypeMemcache:
// Memcache
servers := strings.Split(config.Instance.Cache.Server, ",")
servers := strings.Split(conf.Server, ",")
cacheStore = memcache_store.NewMemcache(
memcache.New(servers...),
store.WithExpiration(time.Duration(config.Instance.Cache.GetExpiresTime())),
store.WithExpiration(cache.ttl),
)
default:
logrus.Fatal("Invalid cache type `" + cacheType + "`, please check config option `cache.type`")
log.Fatal(`Invalid cache type "` + conf.Type + `", please check config option "cache.type"`)
}
cacheInstance := cache.New[any](cacheStore)
cache.instance = lib_cache.New[any](cacheStore)
// marshaler wrapper
// marshaler using VmihailencoMsgpack
// @link https://github.com/vmihailenco/msgpack
// Benchmarks
// @link https://github.com/alecthomas/go_serialization_benchmarks
CACHE = marshaler.New(cacheInstance)
cache.marshal = marshaler.New(cache.instance)
return
return cache, nil
}
+27
View File
@@ -0,0 +1,27 @@
package cache_test
import (
"testing"
"github.com/ArtalkJS/Artalk/internal/cache"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/stretchr/testify/assert"
)
func newCache(t *testing.T) *cache.Cache {
cache, err := cache.New(config.CacheConf{
Enabled: true,
Type: config.CacheTypeBuiltin,
})
if err != nil {
t.Fatal(err)
}
return cache
}
func TestNew(t *testing.T) {
cache := newCache(t)
defer cache.Close()
assert.NotNil(t, cache)
}
-10
View File
@@ -1,10 +0,0 @@
package cache
import (
"github.com/ArtalkJS/Artalk/internal/db"
"gorm.io/gorm"
)
func DB() *gorm.DB {
return db.DB()
}
-111
View File
@@ -1,111 +0,0 @@
package cache
import (
"fmt"
"time"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/sirupsen/logrus"
)
// 缓存预热
func CacheWarmUp() {
// Users
{
start := time.Now()
var items []entity.User
DB().Find(&items)
for _, item := range items {
UserCacheSave(&item)
}
logrus.Debug(fmt.Sprintf("[Users] 缓存完毕 (共 %d 个,耗时:%s)", len(items), time.Since(start)))
}
// Sites
{
start := time.Now()
var items []entity.Site
DB().Find(&items)
for _, item := range items {
SiteCacheSave(&item)
}
logrus.Debug(fmt.Sprintf("[Sites] 缓存完毕 (共 %d 个,耗时:%s)", len(items), time.Since(start)))
}
// Pages
{
start := time.Now()
var items []entity.Page
DB().Find(&items)
for _, item := range items {
PageCacheSave(&item)
}
logrus.Debug(fmt.Sprintf("[Pages] 缓存完毕 (共 %d 个,耗时:%s)", len(items), time.Since(start)))
}
// Comments
{
start := time.Now()
var items []entity.Comment
DB().Find(&items)
for _, item := range items {
CommentCacheSave(&item)
}
logrus.Debug(fmt.Sprintf("[Comments] 缓存完毕 (共 %d 个,耗时:%s)", len(items), time.Since(start)))
}
}
// 清空缓存
func CacheFlushAll() {
// Users
{
var items []entity.User
DB().Find(&items)
for _, item := range items {
UserCacheDel(&item)
}
}
// Sites
{
var items []entity.Site
DB().Find(&items)
for _, item := range items {
SiteCacheDel(&item)
}
}
// Pages
{
var items []entity.Page
DB().Find(&items)
for _, item := range items {
PageCacheDel(&item)
}
}
// Comments
{
var items []entity.Comment
DB().Find(&items)
for _, item := range items {
CommentCacheDel(&item)
}
}
}
+46
View File
@@ -0,0 +1,46 @@
package simple_cache
import (
"sync"
"time"
)
type Cache struct {
data sync.Map
}
func New() *Cache {
return &Cache{}
}
type cacheEntry struct {
value interface{}
expiration time.Time
}
func (c *Cache) Set(key string, value interface{}, expirationParam ...time.Duration) {
var expirationTime time.Time
if len(expirationParam) > 0 && expirationParam[0] > 0 {
expirationTime = time.Now().Add(expirationParam[0])
}
entry := cacheEntry{
value: value,
expiration: expirationTime,
}
c.data.Store(key, entry)
}
func (c *Cache) Get(key string) (interface{}, bool) {
if val, found := c.data.Load(key); found {
entry := val.(cacheEntry)
if entry.expiration.IsZero() || entry.expiration.After(time.Now()) {
return entry.value, true
}
c.data.Delete(key)
}
return nil, false
}
func (c *Cache) Delete(key string) {
c.data.Delete(key)
}
-62
View File
@@ -1,62 +0,0 @@
package captcha
import (
"crypto/hmac"
"crypto/sha256"
"embed"
"encoding/hex"
"io"
"github.com/ArtalkJS/Artalk/internal/config"
)
type Map = map[string]interface{}
type CaptchaPayload struct {
CheckValue string
UserID string
UserIP string
}
type Captcha interface {
Check(CaptchaPayload) (bool, error)
PageParams() Map
}
//go:embed pages/*
var pages embed.FS
func GetIFrameHTML(t config.CaptchaType) ([]byte, error) {
f, err := pages.Open("pages/" + string(t) + ".html")
if err != nil {
return nil, err
}
defer f.Close()
buf, err := io.ReadAll(f)
if err != nil {
return nil, err
}
return buf, nil
}
func NewCaptcha(t config.CaptchaType) Captcha {
switch t {
case config.TypeTurnstile:
return NewTurnstileCaptcha(&config.Instance.Captcha.Turnstile)
case config.TypeReCaptcha:
return NewReCaptcha(&config.Instance.Captcha.ReCaptcha)
case config.TypeHCaptcha:
return NewHCaptcha(&config.Instance.Captcha.HCaptcha)
case config.TypeGeetest:
return NewGeetestCaptcha(&config.Instance.Captcha.Geetest)
default:
panic("Unknown captcha type")
}
}
// hmac-sha256 加密
func hmacEncode(key string, data string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}
+47
View File
@@ -0,0 +1,47 @@
package captcha
import (
"github.com/ArtalkJS/Artalk/internal/config"
)
type Map = map[string]interface{}
type CheckerConf struct {
config.CaptchaConf
User User
}
type User struct {
ID string
IP string
}
type CaptchaType = int
const (
Image CaptchaType = iota
IFrame
)
type Checker interface {
Type() CaptchaType
Check(value string) (bool, error)
Get() ([]byte, error)
}
func NewCaptchaChecker(conf *CheckerConf) Checker {
switch conf.CaptchaType {
case config.TypeImage:
return NewImageChecker(&conf.User)
case config.TypeTurnstile:
return NewTurnstileChecker(&conf.Turnstile, &conf.User)
case config.TypeReCaptcha:
return NewReCaptchaChecker(&conf.ReCaptcha, &conf.User)
case config.TypeHCaptcha:
return NewHCaptchaChecker(&conf.HCaptcha, &conf.User)
case config.TypeGeetest:
return NewGeetestChecker(&conf.Geetest, &conf.User)
default:
panic("Unknown captcha type")
}
}
@@ -2,7 +2,7 @@ package captcha
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
@@ -14,15 +14,17 @@ import (
const GEETEST_API string = "http://gcaptcha4.geetest.com"
type GeetestCaptcha struct {
var _ Checker = (*GeetestCaptchaChecker)(nil)
type GeetestCaptchaChecker struct {
User *User
CaptchaID string
CaptchaKey string
}
var _ Captcha = (*GeetestCaptcha)(nil)
func NewGeetestCaptcha(conf *config.GeetestConf) *GeetestCaptcha {
return &GeetestCaptcha{
func NewGeetestChecker(conf *config.GeetestConf, user *User) *GeetestCaptchaChecker {
return &GeetestCaptchaChecker{
User: user,
CaptchaID: conf.CaptchaID,
CaptchaKey: conf.CaptchaKey,
}
@@ -36,9 +38,9 @@ type GeetestParams struct {
CaptchaOutput string `json:"captcha_output"`
}
func (c *GeetestCaptcha) Check(p CaptchaPayload) (bool, error) {
func (c *GeetestCaptchaChecker) Check(value string) (bool, error) {
var ck GeetestParams
if err := json.Unmarshal([]byte(p.CheckValue), &ck); err != nil {
if err := json.Unmarshal([]byte(value), &ck); err != nil {
return false, err
}
@@ -70,12 +72,16 @@ func (c *GeetestCaptcha) Check(p CaptchaPayload) (bool, error) {
return true, nil
} else {
// 验证失败
return false, errors.New("err reason: " + gjson.GetBytes(respBuf, "reason").String())
return false, fmt.Errorf("err reason: %s", gjson.GetBytes(respBuf, "reason").String())
}
}
func (c *GeetestCaptcha) PageParams() Map {
return Map{
func (c *GeetestCaptchaChecker) Type() CaptchaType {
return IFrame
}
func (c *GeetestCaptchaChecker) Get() ([]byte, error) {
return RenderIFrame("geetest.html", Map{
"gt_id": c.CaptchaID,
}
})
}
@@ -1,7 +1,7 @@
package captcha
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
@@ -13,27 +13,29 @@ import (
const HCAPTCHA_API = "https://api.hcaptcha.com/siteverify"
type HCaptcha struct {
var _ Checker = (*HCaptchaChecker)(nil)
type HCaptchaChecker struct {
User *User
SiteKey string
SecreteKey string
}
var _ Captcha = (*HCaptcha)(nil)
func NewHCaptcha(conf *config.HCaptchaConf) *HCaptcha {
return &HCaptcha{
func NewHCaptchaChecker(conf *config.HCaptchaConf, user *User) *HCaptchaChecker {
return &HCaptchaChecker{
User: user,
SiteKey: conf.SiteKey,
SecreteKey: conf.SecretKey,
}
}
func (c *HCaptcha) Check(p CaptchaPayload) (bool, error) {
func (c *HCaptchaChecker) Check(value string) (bool, error) {
// 构建 POST 请求的参数
values := make(url.Values)
values.Add("secret", c.SecreteKey)
values.Add("response", p.CheckValue)
if p.UserIP != "" {
values.Add("remoteip", p.UserIP)
values.Add("response", value)
if c.User.IP != "" {
values.Add("remoteip", c.User.IP)
}
// 发送 POST 请求
@@ -53,12 +55,16 @@ func (c *HCaptcha) Check(p CaptchaPayload) (bool, error) {
return true, nil
} else {
// 验证失败
return false, errors.New("err reason: " + gjson.GetBytes(respBuf, "error-codes").String())
return false, fmt.Errorf("err reason: %s", gjson.GetBytes(respBuf, "error-codes").String())
}
}
func (c *HCaptcha) PageParams() Map {
return Map{
func (c *HCaptchaChecker) Type() CaptchaType {
return IFrame
}
func (c *HCaptchaChecker) Get() ([]byte, error) {
return RenderIFrame("hcaptcha.html", Map{
"site_key": c.SiteKey,
}
})
}
+29
View File
@@ -0,0 +1,29 @@
package captcha
import (
"github.com/ArtalkJS/Artalk/internal/captcha/image_captcha"
)
var _ Checker = (*ImageChecker)(nil)
type ImageChecker struct {
User *User
}
func NewImageChecker(user *User) *ImageChecker {
return &ImageChecker{
User: user,
}
}
func (c *ImageChecker) Type() CaptchaType {
return Image
}
func (c *ImageChecker) Check(value string) (bool, error) {
return image_captcha.CheckImageCaptchaCode(c.User.IP, value), nil
}
func (c *ImageChecker) Get() ([]byte, error) {
return image_captcha.GetNewImageCaptchaBase64(c.User.IP)
}
@@ -1,7 +1,7 @@
package captcha
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
@@ -13,27 +13,29 @@ import (
const RECAPTCHA_API = "https://www.google.com/recaptcha/api/siteverify"
type ReCaptcha struct {
var _ Checker = (*ReCaptchaChecker)(nil)
type ReCaptchaChecker struct {
User *User
SiteKey string
SecreteKey string
}
var _ Captcha = (*ReCaptcha)(nil)
func NewReCaptcha(conf *config.ReCaptchaConf) *ReCaptcha {
return &ReCaptcha{
func NewReCaptchaChecker(conf *config.ReCaptchaConf, user *User) *ReCaptchaChecker {
return &ReCaptchaChecker{
User: user,
SiteKey: conf.SiteKey,
SecreteKey: conf.SecretKey,
}
}
func (c *ReCaptcha) Check(p CaptchaPayload) (bool, error) {
func (c *ReCaptchaChecker) Check(value string) (bool, error) {
// 构建 POST 请求的参数
values := make(url.Values)
values.Add("secret", c.SecreteKey)
values.Add("response", p.CheckValue)
if p.UserIP != "" {
values.Add("remoteip", p.UserIP)
values.Add("response", value)
if c.User.IP != "" {
values.Add("remoteip", c.User.IP)
}
// 发送 POST 请求
@@ -53,12 +55,16 @@ func (c *ReCaptcha) Check(p CaptchaPayload) (bool, error) {
return true, nil
} else {
// 验证失败
return false, errors.New("err reason: " + gjson.GetBytes(respBuf, "error-codes").String())
return false, fmt.Errorf("err reason: %s", gjson.GetBytes(respBuf, "error-codes").String())
}
}
func (c *ReCaptcha) PageParams() Map {
return Map{
func (c *ReCaptchaChecker) Type() CaptchaType {
return IFrame
}
func (c *ReCaptchaChecker) Get() ([]byte, error) {
return RenderIFrame("recaptcha.html", Map{
"site_key": c.SiteKey,
}
})
}
@@ -1,7 +1,7 @@
package captcha
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
@@ -13,27 +13,29 @@ import (
const TURNSTILE_API = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
type TurnstileCaptcha struct {
var _ Checker = (*TurnstileChecker)(nil)
type TurnstileChecker struct {
User *User
SiteKey string
SecreteKey string
}
var _ Captcha = (*TurnstileCaptcha)(nil)
func NewTurnstileCaptcha(conf *config.TurnstileConf) *TurnstileCaptcha {
return &TurnstileCaptcha{
func NewTurnstileChecker(conf *config.TurnstileConf, user *User) *TurnstileChecker {
return &TurnstileChecker{
User: user,
SiteKey: conf.SiteKey,
SecreteKey: conf.SecretKey,
}
}
func (c *TurnstileCaptcha) Check(p CaptchaPayload) (bool, error) {
func (c *TurnstileChecker) Check(value string) (bool, error) {
// 构建 POST 请求的参数
values := make(url.Values)
values.Add("secret", c.SecreteKey)
values.Add("response", p.CheckValue)
if p.UserIP != "" {
values.Add("remoteip", p.UserIP)
values.Add("response", value)
if c.User.IP != "" {
values.Add("remoteip", c.User.IP)
}
// 发送 POST 请求
@@ -53,12 +55,16 @@ func (c *TurnstileCaptcha) Check(p CaptchaPayload) (bool, error) {
return true, nil
} else {
// 验证失败
return false, errors.New("err reason: " + gjson.GetBytes(respBuf, "error-codes").String())
return false, fmt.Errorf("err reason: %s", gjson.GetBytes(respBuf, "error-codes").String())
}
}
func (c *TurnstileCaptcha) PageParams() Map {
return Map{
func (c *TurnstileChecker) Type() CaptchaType {
return IFrame
}
func (c *TurnstileChecker) Get() ([]byte, error) {
return RenderIFrame("turnstile.html", Map{
"site_key": c.SiteKey,
}
})
}
+32
View File
@@ -0,0 +1,32 @@
package captcha
import (
"bytes"
"embed"
"html/template"
"io"
)
//go:embed pages/*
var pages embed.FS
func RenderIFrame(htmlFilename string, params Map) ([]byte, error) {
f, err := pages.Open("pages/" + htmlFilename)
if err != nil {
return nil, err
}
defer f.Close()
fb, err := io.ReadAll(f)
if err != nil {
return nil, err
}
var tplBuf bytes.Buffer
tpl := template.New("")
tpl.Parse(string(fb))
tpl.Execute(&tplBuf, params)
return tplBuf.Bytes(), nil
}
@@ -0,0 +1,58 @@
package image_captcha
import (
"bytes"
"encoding/base64"
"image/color"
"strings"
"time"
"github.com/ArtalkJS/Artalk/internal/cache/simple_cache"
"github.com/steambap/captcha"
)
const (
CaptchaExpiration = 5 * time.Minute // 验证码 5 分钟内有效
CaptchaCachePrefix = "atk_captcha:"
)
var captchaStore = simple_cache.New()
// 获取对应 IP 图片验证码正确的值
func CheckImageCaptchaCode(ip string, code string) bool {
realVal, isFound := captchaStore.Get(CaptchaCachePrefix + ip)
return isFound && strings.EqualFold(realVal.(string), code)
}
// 获取新验证码 base64 格式图片
// (调用该函数将销毁原有验证码)
func GetNewImageCaptchaBase64(ip string) ([]byte, error) {
// generate a image
pngBuffer := bytes.NewBuffer([]byte{})
data, err := captcha.New(160, 40, func(o *captcha.Options) {
o.FontScale = 1
o.CurveNumber = 2
o.FontDPI = 85.0
o.Noise = 0.7
o.BackgroundColor = color.White
})
if err != nil {
return nil, err
}
if err := data.WriteImage(pngBuffer); err != nil {
return nil, err
}
base64 := "data:image/png;base64," + base64.StdEncoding.EncodeToString(pngBuffer.Bytes())
// save real code
captchaStore.Set(CaptchaCachePrefix+ip, data.Text, CaptchaExpiration)
return []byte(base64), nil
}
// 销毁图片验证码
func InvalidateImageCaptcha(ip string) {
captchaStore.Delete(CaptchaCachePrefix + ip)
}
+14
View File
@@ -0,0 +1,14 @@
package captcha
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
// hmac-sha256 加密
func hmacEncode(key string, data string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}
+3 -3
View File
@@ -2,7 +2,7 @@ package aliyun
import (
"encoding/json"
"errors"
"fmt"
)
type GreenTextConf struct {
@@ -42,10 +42,10 @@ func GreenText(conf GreenTextConf) (bool, error) {
}
if data.Code != 200 {
return false, errors.New(data.Msg)
return false, fmt.Errorf(data.Msg)
}
if len(data.Data) < 1 || len(data.Data[0].Results) < 1 {
return false, errors.New("not expected response: " + string(body))
return false, fmt.Errorf("not expected response: %s", string(body))
}
return (data.Data[0].Results[0].Suggestion == "pass"), nil
+3 -3
View File
@@ -2,7 +2,7 @@ package qcloud
import (
"encoding/json"
"errors"
"fmt"
"strconv"
)
@@ -41,12 +41,12 @@ func TMS(conf TmsConf) (bool, error) {
if data.Response.Error != nil {
errMsg := data.Response.Error.Message
reqID := data.Response.RequestId
return false, errors.New("响应错误 err_msg=" + strconv.Quote(errMsg) + " request_id=" + reqID)
return false, fmt.Errorf("响应错误 err_msg=%s request_id= %s", strconv.Quote(errMsg), reqID)
}
suggestion := data.Response.Suggestion
if suggestion == "" {
return false, errors.New("suggestion value is empty")
return false, fmt.Errorf("suggestion value is empty")
}
return (suggestion == "Pass"), nil
+214
View File
@@ -0,0 +1,214 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
"unicode"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/utils"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
)
func New() *Config {
conf := &Config{}
return conf
}
// 从文件中创建配置实例
func NewFromFile(cfgFile string) (*Config, error) {
kf := koanf.New(".")
// load yaml config
if err := kf.Load(file.Provider(cfgFile), yaml.Parser()); err != nil {
return nil, fmt.Errorf("config file read error: %w", err)
}
// load environment variables and merge into the loaded config
const envPrefix = "ATK_"
if err := kf.Load(env.Provider(envPrefix, ".", func(s string) string {
// FOO__BAR -> foo_bar to handle dash in config names
s = strings.ToLower(strings.TrimPrefix(s, envPrefix))
s = strings.ReplaceAll(s, "__", "-")
s = strings.ReplaceAll(s, "_", ".")
return strings.ReplaceAll(s, "-", "_")
}), nil); err != nil {
return nil, fmt.Errorf("config environment variable parse error: %w", err)
}
// create new config instance
conf := &Config{
cfgFile: cfgFile,
}
// use koanf parser to decode config file to instance
if err := kf.Unmarshal("", conf); err != nil {
return nil, fmt.Errorf("config file parse error: %w", err)
}
// patch config
{
conf.historyPatch()
conf.normalPatch()
conf.i18nPatch()
conf.ipRegionPatch()
}
return conf, nil
}
func (conf *Config) GetCfgFileLoaded() string {
return conf.cfgFile
}
// 配置修补
func (conf *Config) normalPatch() {
// 检查 app_key 是否设置
if strings.TrimSpace(conf.AppKey) == "" {
conf.AppKey = utils.RandomString(16)
log.Warn("config `app_key` is not set, now it is random value")
}
// 检查时区
conf.TimeZone = strings.TrimSpace(conf.TimeZone)
if conf.TimeZone == "" {
conf.TimeZone = "Local"
log.Warn("config `timezone` is not set, now it is: " + strconv.Quote(conf.TimeZone))
}
// 默认站点配置
conf.SiteDefault = strings.TrimSpace(conf.SiteDefault)
if conf.SiteDefault == "" {
conf.SiteDefault = "Default Site"
log.Warn("config `site_default` is not set, now it is: " + strconv.Quote(conf.SiteDefault))
}
// 缓存配置
if conf.Cache.Type == "" {
// 默认使用内建缓存
conf.Cache.Type = CacheTypeBuiltin
}
// 配置文件 alias 处理
if conf.Captcha.ActionLimit == 0 {
conf.Captcha.Always = true
}
// 管理员邮件通知配置继承
if conf.AdminNotify.Email.MailSubject == "" {
if conf.AdminNotify.NotifySubject != "" {
conf.AdminNotify.Email.MailSubject = conf.AdminNotify.NotifySubject
} else if conf.Email.MailSubject != "" {
conf.AdminNotify.Email.MailSubject = conf.Email.MailSubject
}
}
// 默认待审模式下开启管理员通知嘈杂模式,保证管理员能看到待审核文章
if conf.Moderator.PendingDefault {
conf.AdminNotify.NoiseMode = true
}
// 默认将验证码类型设置为 image
if strings.TrimSpace(string(conf.Captcha.CaptchaType)) == "" {
conf.Captcha.CaptchaType = TypeImage
}
// 图片上传存放路径默认设置
if conf.ImgUpload.Path == "" {
conf.ImgUpload.Path = "./data/artalk-img/"
log.Warn("[Image Upload] img_upload.path is not configured, using the default value: " + strconv.Quote(conf.ImgUpload.Path))
}
}
// 多语言配置修补
func (conf *Config) i18nPatch() {
if conf.Locale == "" {
conf.Locale = "en"
// zh-CN default patch (for 历史兼容)
// 判断配置文件中是否有中文,若有中文则将 locale 设置为 zh-CN
if confRaw, err := os.ReadFile(conf.GetCfgFileLoaded()); err == nil {
containsHan := false
for _, runeValue := range string(confRaw) {
if unicode.Is(unicode.Han, runeValue) {
containsHan = true
break
}
}
if containsHan {
conf.Locale = "zh-CN"
}
}
log.Warn("config `locale` is not set, now it is: " + strconv.Quote(conf.Locale))
} else if conf.Locale == "zh" {
conf.Locale = "zh-CN"
}
}
// IP属地功能配置修补
func (conf *Config) ipRegionPatch() {
// IP 属地默认数据文件
if conf.IPRegion.DBPath == "" {
conf.IPRegion.DBPath = "./data/ip2region.xdb"
}
// 检测配置文件是否存在
if !utils.CheckFileExist(conf.IPRegion.DBPath) {
log.Warn("未找到 IP 数据库文件:" + strconv.Quote(conf.IPRegion.DBPath) + ",IP 归属地功能已禁用," +
"参考链接:https://artalk.js.org/guide/frontend/ip-region.html")
conf.IPRegion.Enabled = false
}
// 默认精确到省
if conf.IPRegion.Precision == "" {
conf.IPRegion.Precision = string(IPRegionProvince)
}
}
// 配置修补 for 历史版本
func (conf *Config) historyPatch() {
if conf.Captcha.ActionTimeout != 0 {
log.Warn("The config option `captcha.action_timeout` is deprecated, please use `captcha.action_reset` instead")
if conf.Captcha.ActionReset == 0 {
conf.Captcha.ActionReset = conf.Captcha.ActionTimeout
}
}
if len(conf.AllowOrigins) != 0 {
log.Warn("The config option `allow_origins` is deprecated, please use `trusted_domains` instead")
if len(conf.TrustedDomains) == 0 {
conf.TrustedDomains = conf.AllowOrigins
}
}
// @version < 2.2.0
if conf.Notify != nil {
log.Warn("The config option `notify` is deprecated, please use `admin_notify` instead")
conf.AdminNotify = *conf.Notify
}
if conf.AdminNotify.Email == nil {
conf.AdminNotify.Email = &AdminEmailConf{
Enabled: true, // 默认开启管理员邮件通知
}
}
if conf.Email.MailSubjectToAdmin != "" {
log.Warn("The config option `email.mail_subject_to_admin` is deprecated, please use `admin_notify.email.mail_subject` instead")
conf.AdminNotify.Email.MailSubject = conf.Email.MailSubjectToAdmin
}
}
// 尝试查找配置文件
func RetrieveConfigFile() string {
for _, v := range CONF_DEFAULT_FILENAMES {
if utils.CheckFileExist(v) {
return v
}
}
return ""
}
+26 -7
View File
@@ -1,9 +1,11 @@
package config
import "time"
import (
"time"
)
// Config 配置
// @link https://godoc.org/github.com/mitchellh/mapstructure
// @link https://github.com/knadh/koanf
type Config struct {
AppKey string `koanf:"app_key" json:"app_key"` // 加密密钥
Debug bool `koanf:"debug" json:"debug"` // 调试模式
@@ -11,10 +13,9 @@ type Config struct {
TimeZone string `koanf:"timezone" json:"timezone"` // 时区
Host string `koanf:"host" json:"host"` // HTTP Server 监听 IP
Port int `koanf:"port" json:"port"` // HTTP Server 监听 Port
DB DBConf `koanf:"db" json:"db"` // 数据文件
DB DBConf `koanf:"db" json:"db"` // 数据库配置
Cache CacheConf `koanf:"cache" json:"cache"` // 缓存
Log LogConf `koanf:"log" json:"log"` // 日志文件
AllowOrigins []string `koanf:"allow_origins" json:"-"` // @deprecated 已废弃 (请使用 TrustedDomains)
TrustedDomains []string `koanf:"trusted_domains" json:"trusted_domains"` // 可信任的域名 (新)
SSL SSLConf `koanf:"ssl" json:"ssl"` // SSL
SiteDefault string `koanf:"site_default" json:"site_default"` // 默认站点名(当请求无指定 site_name 时使用)
@@ -27,8 +28,19 @@ type Config struct {
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"` // 其他通知方式
Notify *AdminNotifyConf `koanf:"notify" json:"-"` // @deprecated 已废弃 (请使用 AdminNotify)
Frontend map[string]interface{} `koanf:"frontend" json:"frontend"`
// deprecated options
// (only for unmarshal, please not reference)
// ---------------------------
AllowOrigins []string `koanf:"allow_origins" json:"-"` // @deprecated 已废弃 (请使用 TrustedDomains)
Notify *AdminNotifyConf `koanf:"notify" json:"-"` // @deprecated 已废弃 (请使用 AdminNotify)
// system runtime produce data
// ---------------------------
cfgFile string
}
type DBConf struct {
@@ -48,7 +60,7 @@ type DBConf struct {
}
type CacheConf struct {
Enabled bool // 配置文件不允许修改
Enabled bool `koanf:"enabled" json:"enabled"`
Type CacheType `koanf:"type" json:"type"`
Expires int `koanf:"expires" json:"expires"` // 过期时间
WarmUp bool `koanf:"warm_up" json:"warm_up"` // 启动时缓存预热
@@ -219,7 +231,6 @@ const (
CacheTypeBuiltin CacheType = "builtin" // 内建缓存
CacheTypeRedis CacheType = "redis"
CacheTypeMemcache CacheType = "memcache"
CacheTypeDisabled CacheType = "disabled" // 关闭缓存
)
type EmailSenderType string
@@ -250,6 +261,14 @@ type IPRegionConf struct {
Precision string `koanf:"precision" json:"precision"` // 显示精度
}
type IPRegionPrecision string
const (
IPRegionProvince IPRegionPrecision = "province"
IPRegionCity IPRegionPrecision = "city"
IPRegionCountry IPRegionPrecision = "country"
)
type ImgUploadConf struct {
Enabled bool `koanf:"enabled" json:"enabled"` // 总开关
Path string `koanf:"path" json:"path"` // 图片存放路径
+14 -2
View File
@@ -1,6 +1,18 @@
package config
// Artalk 保留关键字
// 默认配置文件名
var CONF_DEFAULT_FILENAMES = [...]string{
"artalk.yml",
"artalk-go.yml", // for 向下兼容
"config.yml",
"conf.yml",
// .yaml extension
"artalk.yaml",
"artalk-go.yaml",
"config.yml",
"conf.yml",
}
// 所有站点
const ATK_SITE_ALL = "__ATK_SITE_ALL"
@@ -14,4 +26,4 @@ const CTX_KEY_ATK_SITE_NAME = "atk_site_name"
const CTX_KEY_ATK_SITE_ALL = "atk_site_all"
// 图片上传目录路由重写路径
var IMG_UPLOAD_PUBLIC_PATH = "/static/images"
const IMG_UPLOAD_PUBLIC_PATH = "/static/images"
-131
View File
@@ -1,131 +0,0 @@
package config
import (
"strings"
"time"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/file"
"github.com/sirupsen/logrus"
)
// 默认配置文件名
const DEFAULT_CONF_FILE = "artalk.yml"
var (
kf = koanf.New(".")
parser = yaml.Parser()
// 配置实例
Instance *Config
cfgFileLoaded string
)
func GetCfgFileLoaded() string {
return cfgFileLoaded
}
// Init 初始化配置
func Init(cfgFile string) {
// load yaml config
if err := kf.Load(file.Provider(cfgFile), parser); err != nil {
logrus.Errorln(err)
logrus.Fatal("Config file read error")
}
Instance = &Config{}
if err := kf.Unmarshal("", Instance); err != nil {
logrus.Errorln(err)
logrus.Fatal("Config file parse error")
}
cfgFileLoaded = cfgFile
// 后续处理
postInit()
}
func postInit() {
// 检查 app_key 是否设置
if strings.TrimSpace(Instance.AppKey) == "" {
logrus.Fatal("Please check config file and set an `app_key` for data encryption")
}
// 设置时区
if strings.TrimSpace(Instance.TimeZone) == "" {
logrus.Fatal("Please check config file and set `timezone`")
}
denverLoc, _ := time.LoadLocation(Instance.TimeZone)
time.Local = denverLoc
// 默认站点配置
Instance.SiteDefault = strings.TrimSpace(Instance.SiteDefault)
if Instance.SiteDefault == "" {
logrus.Fatal("Please check config file and set `site_default`")
}
// 缓存配置
if Instance.Cache.Type == "" {
// 默认使用内建缓存
Instance.Cache.Type = CacheTypeBuiltin
}
if Instance.Cache.Type != CacheTypeDisabled {
// 非缓存禁用模式,Enabled = true
Instance.Cache.Enabled = true
}
// 配置文件 alias 处理
if Instance.Captcha.ActionLimit == 0 {
Instance.Captcha.Always = true
}
/* 检查废弃需更新配置 */
if Instance.Captcha.ActionTimeout != 0 {
logrus.Warn("The config option `captcha.action_timeout` is deprecated, please use `captcha.action_reset` instead")
if Instance.Captcha.ActionReset == 0 {
Instance.Captcha.ActionReset = Instance.Captcha.ActionTimeout
}
}
if len(Instance.AllowOrigins) != 0 {
logrus.Warn("The config option `allow_origins` is deprecated, please use `trusted_domains` instead")
if len(Instance.TrustedDomains) == 0 {
Instance.TrustedDomains = Instance.AllowOrigins
}
}
// @version < 2.2.0
if Instance.Notify != nil {
logrus.Warn("The config option `notify` is deprecated, please use `admin_notify` instead")
Instance.AdminNotify = *Instance.Notify
}
if Instance.AdminNotify.Email == nil {
Instance.AdminNotify.Email = &AdminEmailConf{
Enabled: true, // 默认开启管理员邮件通知
}
}
if Instance.Email.MailSubjectToAdmin != "" {
logrus.Warn("The config option `email.mail_subject_to_admin` is deprecated, please use `admin_notify.email.mail_subject` instead")
Instance.AdminNotify.Email.MailSubject = Instance.Email.MailSubjectToAdmin
}
// 管理员邮件通知配置继承
if Instance.AdminNotify.Email.MailSubject == "" {
if Instance.AdminNotify.NotifySubject != "" {
Instance.AdminNotify.Email.MailSubject = Instance.AdminNotify.NotifySubject
} else if Instance.Email.MailSubject != "" {
Instance.AdminNotify.Email.MailSubject = Instance.Email.MailSubject
}
}
// 默认待审模式下开启管理员通知嘈杂模式,保证管理员能看到待审核文章
if Instance.Moderator.PendingDefault {
Instance.AdminNotify.NoiseMode = true
}
// 默认将验证码类型设置为 image
if strings.TrimSpace(string(Instance.Captcha.CaptchaType)) == "" {
Instance.Captcha.CaptchaType = TypeImage
}
}
+36
View File
@@ -0,0 +1,36 @@
package config
import (
"fmt"
"io"
"github.com/ArtalkJS/Artalk/internal/pkged"
)
const CONF_TPL_DEFAULT_PATH = "conf/artalk.example.yml"
// Template is get config template file with specified locale
func Template(locale string) string {
var filename string
if locale == "en" || locale == "" {
filename = CONF_TPL_DEFAULT_PATH
} else {
filename = fmt.Sprintf("conf/artalk.example.%s.yml", locale)
}
f, err := pkged.FS().Open(filename)
if err != nil {
// locale not found
f, err = pkged.FS().Open(CONF_TPL_DEFAULT_PATH)
if err != nil {
panic(err)
}
}
fileStr, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return string(fileStr)
}
+270
View File
@@ -0,0 +1,270 @@
package core
import (
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/ArtalkJS/Artalk/internal/cache"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/dao"
"github.com/ArtalkJS/Artalk/internal/db"
"github.com/ArtalkJS/Artalk/internal/hook"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/pkged"
)
type App struct {
conf *config.Config
dao *dao.Dao
cache *cache.Cache
service *map[string]Service
onTerminate *hook.Hook[*TerminateEvent]
}
func NewApp(conf *config.Config) *App {
app := &App{
conf: conf,
service: &map[string]Service{},
onTerminate: &hook.Hook[*TerminateEvent]{},
}
app.injectDefaultServices()
app.registerDefaultHooks()
return app
}
func (app *App) injectDefaultServices() {
// 请勿依赖注入顺序
AppInject[*EmailService](app, NewEmailService(app))
AppInject[*IPRegionService](app, NewIPRegionService(app))
AppInject[*NotifyService](app, NewNotifyService(app))
AppInject[*AntiSpamService](app, NewAntiSpamService(app))
}
func (app *App) registerDefaultHooks() {
app.OnTerminate().Add(func(e *TerminateEvent) error {
app.ResetBootstrapState()
return nil
})
}
var mutex = sync.Mutex{}
// Bootstrap implements App.
func (app *App) Bootstrap() error {
mutex.Lock()
defer mutex.Unlock()
if app.Conf() == nil {
return fmt.Errorf("app.conf cannot be nil while bootstrap")
}
// 时区设置
denverLoc, _ := time.LoadLocation(app.Conf().TimeZone)
time.Local = denverLoc
// i18n
app.initI18n()
// log
log.LoadGlobal(log.Options{
IsDiscard: !app.Conf().Log.Enabled,
IsDebug: app.Conf().Debug,
LogFile: app.Conf().Log.Filename,
})
// DAO
if app.dao == nil {
if err := app.initDao(); err != nil {
return err
}
}
// cache
if app.Conf().Cache.Enabled {
// cache
if err := app.initCache(); err != nil {
return err
}
// load cache plugin on dao
app.dao.SetCache(dao.NewCacheAdaptor(app.Cache()))
// 缓存预热
if app.Conf().Cache.WarmUp {
app.Dao().CacheWarmUp()
}
}
// keep config file and databases consistent
app.syncFromConf()
// 初始化 services(请勿依赖初始化顺序)
for name, s := range *app.service {
if err := s.Init(); err != nil {
return fmt.Errorf("Service %s init error: %w", name, err)
}
}
return nil
}
func (app *App) ResetBootstrapState() error {
// close database
if app.Dao() != nil {
sqlDB, _ := app.Dao().DB().DB()
if err := sqlDB.Close(); err != nil {
return err
}
}
// close cache
if app.Cache() != nil {
app.Cache().Close()
}
app.dao = nil
app.cache = nil
// call service release funcs
if app.service != nil {
for name, s := range *app.service {
if err := s.Dispose(); err != nil {
return fmt.Errorf("service %s release error: %w", name, err)
}
}
}
return nil
}
func (app *App) Inject(name string, service Service) {
(*app.service)[name] = service
}
// @see https://github.com/golang/go/issues/55006
// @see https://github.com/golang/go/issues/49085
func AppInject[T Service](app *App, service T) {
app.Inject(genServiceName[T](), service)
}
func (app *App) Service(name string) (Service, error) {
if app.service == nil {
return nil, fmt.Errorf("services map is nil")
}
if _, isExits := (*app.service)[name]; !isExits {
return nil, fmt.Errorf("service %s not found", name)
}
return (*app.service)[name], nil
}
func AppService[T Service](app *App) (T, error) {
if app == nil {
var zero T
return zero, fmt.Errorf("app is nil")
}
service, err := app.Service(genServiceName[T]())
if err != nil {
var zero T
return zero, err
}
return service.(T), nil
}
func (app *App) Conf() *config.Config {
return app.conf
}
func (app *App) SetConf(conf *config.Config) {
app.conf = conf
}
func (app *App) Dao() *dao.Dao {
return app.dao
}
func (app *App) SetDao(dao *dao.Dao) {
app.dao = dao
}
func (app *App) Cache() *cache.Cache {
return app.cache
}
func (app *App) Restart() error {
// optimistically reset the app bootstrap state
if err := app.ResetBootstrapState(); err != nil {
return err
}
// re-bootstrap
if err := app.Bootstrap(); err != nil {
return err
}
return nil
}
func (app *App) ConfTpl() string {
if app.Conf() == nil {
return config.Template("en")
}
return config.Template(strings.TrimSpace(app.Conf().Locale))
}
// -------------------------------------------------------------------
// Internal Initializations
// -------------------------------------------------------------------
func (app *App) initI18n() {
if pkged.FS() == nil {
log.Warn("i18n locales not load because the embed fs not found")
return
}
i18n.Load(app.Conf().Locale, func(locale string) ([]byte, error) {
file, err := pkged.FS().Open(fmt.Sprintf("i18n/%s.yml", locale))
if err != nil {
return nil, err
}
return io.ReadAll(file)
})
}
func (app *App) initCache() error {
cache, err := cache.New(app.conf.Cache)
if err != nil {
return err
}
app.cache = cache
return nil
}
func (app *App) initDao() error {
// create new db instance
dbInstance, err := db.NewDB(app.conf.DB)
if err != nil {
return fmt.Errorf("db init err: %w", err)
}
// create new dao instance
app.SetDao(dao.NewDao(dbInstance))
return nil
}
// -------------------------------------------------------------------
// Hooks
// -------------------------------------------------------------------
func (app *App) OnTerminate() *hook.Hook[*TerminateEvent] {
return app.onTerminate
}
+99
View File
@@ -0,0 +1,99 @@
package core
import (
"testing"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/stretchr/testify/assert"
)
type MockService struct{}
func (s *MockService) Init() error { return nil }
func (s *MockService) Dispose() error { return nil }
func TestNewApp(t *testing.T) {
conf := &config.Config{
// Initialize config fields for testing.
}
app := NewApp(conf)
assert.NotNil(t, app)
assert.Equal(t, conf, app.conf)
assert.NotNil(t, app.service)
}
func TestAppBootstrap(t *testing.T) {
conf := &config.Config{
Cache: config.CacheConf{
Enabled: true,
Type: config.CacheTypeBuiltin,
},
DB: config.DBConf{
Type: config.TypeSQLite,
Dsn: "file::memory:?cache=shared",
},
}
// create app instance
app := NewApp(conf)
defer app.ResetBootstrapState()
// bootstrap
if err := app.Bootstrap(); err != nil {
t.Fatal(err)
}
assert.Equal(t, conf, app.conf)
assert.NotNil(t, app.dao)
assert.NotNil(t, app.cache)
assert.NotNil(t, app.service)
}
func TestAppInjectAndService(t *testing.T) {
app := &App{
service: &map[string]Service{},
}
// inject
mockService := &MockService{}
AppInject[*MockService](app, mockService)
// get
gotService, err := AppService[*MockService](app)
if assert.NoError(t, err) {
assert.NotNil(t, gotService)
assert.Equal(t, mockService, gotService)
}
// err test
t.Run("AccessNilApp", func(t *testing.T) {
var app *App
_, err := AppService[*MockService](app)
assert.Error(t, err)
})
t.Run("AccessNilService", func(t *testing.T) {
app := &App{
service: &map[string]Service{},
}
_, err := AppService[*MockService](app)
assert.Error(t, err)
})
t.Run("AccessNilServicesMap", func(t *testing.T) {
app := &App{
service: nil,
}
_, err := AppService[*MockService](app)
assert.Error(t, err)
})
}
func TestApp_OnTerminate(t *testing.T) {
// Prepare a mock app instance with necessary fields
app := NewApp(&config.Config{})
hook := app.OnTerminate()
assert.NotNil(t, hook)
}
@@ -3,18 +3,16 @@ package core
import (
"strings"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/query"
)
func SyncFromConf() {
func (app *App) syncFromConf() {
// 初始化默认站点
query.FindCreateSite(config.Instance.SiteDefault)
app.Dao().FindCreateSite(app.Conf().SiteDefault)
// 导入配置文件的管理员用户
for _, admin := range config.Instance.AdminUsers {
user := query.FindUser(admin.Name, admin.Email)
for _, admin := range app.Conf().AdminUsers {
user := app.Dao().FindUser(admin.Name, admin.Email)
receiveEmail := true // 默认允许接收邮件
if admin.ReceiveEmail != nil {
receiveEmail = *admin.ReceiveEmail
@@ -33,7 +31,7 @@ func SyncFromConf() {
ReceiveEmail: receiveEmail,
SiteNames: strings.Join(admin.Sites, ","),
}
query.CreateUser(&user)
app.dao.CreateUser(&user)
} else {
// update
user.Name = admin.Name
@@ -46,7 +44,7 @@ func SyncFromConf() {
user.IsInConf = true
user.ReceiveEmail = receiveEmail
user.SiteNames = strings.Join(admin.Sites, ",")
query.UpdateUser(&user)
app.dao.UpdateUser(&user)
}
}
@@ -55,7 +53,7 @@ func SyncFromConf() {
// lib.DB.Model(&User{}).Where(&User{IsInConf: true}).Find(&dbAdminUsers)
// for _, dbU := range dbAdminUsers {
// isUserExist := func() bool {
// for _, confU := range config.Instance.AdminUsers {
// for _, confU := range app.Conf().AdminUsers {
// // 忽略大小写比较
// if strings.EqualFold(confU.Name, dbU.Name) && strings.EqualFold(confU.Email, dbU.Email) {
// return true
-46
View File
@@ -1,46 +0,0 @@
package core
import (
"fmt"
"io"
"strings"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/pkged"
)
// GetConfTpl is get config template file with specified locale
func GetConfTpl(localeParam ...string) string {
var locale string
if len(localeParam) > 0 {
locale = strings.TrimSpace(localeParam[0])
}
if locale == "" && config.Instance != nil {
locale = strings.TrimSpace(config.Instance.Locale)
}
const CONF_DEFAULT = "conf/artalk.example.yml"
var filename string
if locale == "en" || locale == "" {
filename = CONF_DEFAULT
} else {
filename = fmt.Sprintf("conf/artalk.example.%s.yml", locale)
}
f, err := pkged.FS().Open(filename)
if err != nil {
// locale not found
f, err = pkged.FS().Open(CONF_DEFAULT)
if err != nil {
panic(err)
}
}
fileStr, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return string(fileStr)
}
-195
View File
@@ -1,195 +0,0 @@
package core
import (
"io"
"os"
"strconv"
"sync"
"unicode"
"github.com/ArtalkJS/Artalk/internal/cache"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/db"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/ip_region"
"github.com/ArtalkJS/Artalk/internal/notify_launcher"
"github.com/rifflock/lfshook"
"github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
)
var firstLoad = true
var mutex = sync.Mutex{}
// 装载核心功能
func LoadCore(cfgFile string, workDir string) {
mutex.Lock()
defer mutex.Unlock()
firstLoad = false
initConfig(cfgFile, workDir)
initI18n()
initLog()
initCache()
initDB()
notify_launcher.Init() // 初始化 Notify 发射台
initIPRegion()
// 首次 Load
if firstLoad {
// 缓存预热
if config.Instance.Cache.Enabled && config.Instance.Cache.WarmUp {
cache.CacheWarmUp()
}
// 异步加载
// go func() {
// entity.CacheWarmUp()
// }()
}
}
// 仅装载配置
func LoadConfOnly(cfgFile string, workDir string) {
initConfig(cfgFile, workDir)
}
// 1. 初始化配置
func initConfig(cfgFile string, workDir string) {
// 切换工作目录
if workDir != "" {
if err := os.Chdir(workDir); err != nil {
logrus.Fatal("Working directory change error: ", err)
}
}
if cfgFile == "" {
cfgFile = config.DEFAULT_CONF_FILE
// 默认配置文件名 "artalk-go.yml"(for 向下兼容)
if _, err := os.Stat("artalk-go.yml"); err == nil {
cfgFile = "artalk-go.yml"
}
}
// 自动生成新配置文件
if !CheckFileExist(cfgFile) {
Gen("config", cfgFile, false)
}
config.Init(cfgFile)
}
func initI18n() {
if config.Instance.Locale == "" {
config.Instance.Locale = "en"
// zh-CN default patch
// 判断配置文件中是否有中文,若有中文则将 locale 设置为 zh-CN
if confRaw, err := os.ReadFile(config.GetCfgFileLoaded()); err == nil {
containsHan := false
for _, runeValue := range string(confRaw) {
if unicode.Is(unicode.Han, runeValue) {
containsHan = true
break
}
}
if containsHan {
config.Instance.Locale = "zh-CN"
}
}
logrus.Warn("config `locale` is not set, now it is: " + strconv.Quote(config.Instance.Locale))
} else if config.Instance.Locale == "zh" {
config.Instance.Locale = "zh-CN"
}
i18n.Init(config.Instance.Locale)
}
// 2. 初始化日志
func initLog() {
logrus.New()
if !config.Instance.Log.Enabled {
logrus.SetOutput(io.Discard)
return
}
// 命令行输出格式
stdFormatter := &prefixed.TextFormatter{
DisableTimestamp: true,
ForceFormatting: true,
ForceColors: true,
DisableColors: false,
}
logrus.SetFormatter(stdFormatter)
logrus.SetOutput(os.Stdout)
if config.Instance.Debug {
logrus.SetLevel(logrus.DebugLevel)
} else {
logrus.SetLevel(logrus.InfoLevel)
}
// 日志输出到文件
if config.Instance.Log.Filename != "" {
fileFormatter := &prefixed.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02.15:04:05.000000",
ForceFormatting: true,
ForceColors: false,
DisableColors: true,
}
pathMap := lfshook.PathMap{
logrus.InfoLevel: config.Instance.Log.Filename,
logrus.DebugLevel: config.Instance.Log.Filename,
logrus.ErrorLevel: config.Instance.Log.Filename,
}
newHooks := make(logrus.LevelHooks)
newHooks.Add(lfshook.NewHook(
pathMap,
fileFormatter,
))
//logrus.AddHook(lfshook.NewHook()) // 使用 Replace 而不使用 Add
logrus.StandardLogger().ReplaceHooks(newHooks)
}
}
// 3. 初始化缓存
func initCache() {
err := cache.OpenCache()
if err != nil {
logrus.Error("[Cache] ", "Init cache error: ", err)
os.Exit(1)
}
}
// 4. 初始化数据库
func initDB() {
db.InitDB()
db.MigrateModels()
SyncFromConf()
}
func initIPRegion() {
irConf := &config.Instance.IPRegion
if !irConf.Enabled {
return
}
if irConf.DBPath == "" {
irConf.DBPath = "./data/ip2region.xdb"
}
if irConf.Precision == "" {
// 默认精确到省
irConf.Precision = string(ip_region.Province)
}
ip_region.Init(irConf.DBPath) // 初始化 IP 归属地查询
ip_region.SetPrecision(ip_region.Precision(irConf.Precision)) // 精度设置
}
+13
View File
@@ -0,0 +1,13 @@
package core
// -------------------------------------------------------------------
// Event data
// -------------------------------------------------------------------
type BootstrapEvent struct {
App *App
}
type TerminateEvent struct {
App *App
}
+14 -28
View File
@@ -3,18 +3,18 @@ package core
import (
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"strings"
"github.com/ArtalkJS/Artalk/internal/config"
"github.com/ArtalkJS/Artalk/internal/i18n"
"github.com/ArtalkJS/Artalk/internal/log"
"github.com/ArtalkJS/Artalk/internal/pkged"
"github.com/sirupsen/logrus"
"github.com/ArtalkJS/Artalk/internal/utils"
)
func Gen(genType string, specificPath string, overwrite bool) {
// check if generate config file
isGenConf := false
if genType == "config" || genType == "conf" || genType == "artalk.yml" {
@@ -25,9 +25,10 @@ func Gen(genType string, specificPath string, overwrite bool) {
// get generation content
var fileStr string
if isGenConf {
fileStr = GetConfTpl()
// TODO detect the user env language
fileStr = config.Template("en")
// gen random `app_key`
appKey := RandStringRunes(16)
appKey := utils.RandomStringWithAlphabet(16, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*1234567890")
fileStr = strings.Replace(fileStr, `app_key: ""`, fmt.Sprintf(`app_key: "%s"`, appKey), 1)
} else {
fileStr = getEmbedFile(genType)
@@ -40,53 +41,38 @@ func Gen(genType string, specificPath string, overwrite bool) {
absPath, err := filepath.Abs(genFullPath)
if err != nil {
logrus.Fatal(err)
log.Fatal(err)
}
if s, err := os.Stat(absPath); err == nil && s.IsDir() {
absPath = filepath.Join(absPath, filepath.Base(genType))
}
if CheckFileExist(absPath) && !overwrite {
logrus.Fatal(i18n.T("{{name}} already exists", map[string]interface{}{"name": i18n.T("File")}) + ": " + absPath)
if utils.CheckFileExist(absPath) && !overwrite {
log.Fatal(i18n.T("{{name}} already exists", map[string]interface{}{"name": i18n.T("File")}) + ": " + absPath)
}
dst, err := os.Create(absPath)
if err != nil {
logrus.Fatal("Failed to create target file: ", err)
log.Fatal("Failed to create target file: ", err)
}
defer dst.Close()
if _, err = dst.Write([]byte(fileStr)); err != nil {
logrus.Fatal("Failed to write target file: ", err)
log.Fatal("Failed to write target file: ", err)
}
logrus.Info("File Generated: " + absPath)
}
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*1234567890")
func RandStringRunes(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}
func CheckFileExist(path string) bool {
_, err := os.Stat(path)
return err == nil
log.Info("File Generated: " + absPath)
}
func getEmbedFile(filename string) string {
file, err := pkged.FS().Open(strings.TrimPrefix(filename, "/"))
if err != nil {
logrus.Fatal("Invalid built-in resource `"+filename+"`: ", err)
log.Fatal("Invalid built-in resource `"+filename+"`: ", err)
}
buf, err := io.ReadAll(file)
if err != nil {
logrus.Fatal("Read built-in resources `"+filename+"` error: ", err)
log.Fatal("Read built-in resources `"+filename+"` error: ", err)
}
return string(buf)
+21
View File
@@ -0,0 +1,21 @@
package core
import "fmt"
type Service interface {
Init() error
Dispose() error
}
func genServiceName[T any]() string {
var t T
// struct
name := fmt.Sprintf("%T", t)
if name != "<nil>" {
return name
}
// interface
return fmt.Sprintf("%T", new(T))
}
Loaded 100 of 271 files, more files were not shown because too many files have changed in this diff. Show more