Compare commits

...
87 Commits
Author SHA1 Message Date
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
253 changed files with 10420 additions and 7277 deletions

No files matched your search

+5 -5
View File
@@ -9,10 +9,10 @@ executors:
- image: cimg/base:stable
node:
docker:
- image: 'cimg/node:18.15.0'
- 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@8.1.1
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.3'
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
+1 -1
View File
@@ -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
-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)
+95
View File
@@ -1,4 +1,99 @@
## [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
+1 -1
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)
+3 -3
View File
@@ -1,12 +1,12 @@
### build Artalk
FROM golang:1.20.3-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@8.1.1
&& 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 ..."`
+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;
+44 -48
View File
@@ -2,7 +2,6 @@ package cmd
import (
"bufio"
"errors"
"fmt"
"os"
"strings"
@@ -11,63 +10,60 @@ import (
"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 *core.App) *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 +81,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 +103,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
+214
View File
@@ -0,0 +1,214 @@
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")
cmd.eagerParseFlags() // parse base flags
// 切换工作目录
if cmd.workDir != "" {
if err := os.Chdir(cmd.workDir); err != nil {
log.Fatal("Working directory change error: ", err)
}
}
// initialize app instance
config, err := getConfig(cmd.cfgFile)
if err != nil {
log.Fatal("Config fail: ", err)
}
cmd.App = core.NewApp(config)
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) {
if err := atk.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.App))
atk.addCommand(NewAdminCommand(atk.App))
atk.addCommand(NewExportCommand(atk.App))
atk.addCommand(NewImportCommand(atk.App))
atk.addCommand(NewConfigCommand(atk.App))
atk.addCommand(NewGenCommand(atk.App))
atk.addCommand(NewUpgradeCommand(atk.App))
atk.addCommand(NewVersionCommand(atk.App))
}
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
// ===================
return atk.OnTerminate().Trigger(&core.TerminateEvent{
App: atk.App,
})
}
// 获取配置
func getConfig(cfgFile string) (*config.Config, error) {
// 尝试查找配置文件
if cfgFile == "" {
cfgFile = config.RetrieveConfigFile()
}
// 自动生成新配置文件
if cfgFile == "" {
cfgFile = config.CONF_DEFAULT_FILENAMES[0]
core.Gen("config", cfgFile, false)
}
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)
}
+25
View File
@@ -0,0 +1,25 @@
package cmd
import (
"encoding/json"
"fmt"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/spf13/cobra"
)
func NewConfigCommand(app *core.App) *cobra.Command {
configCmd := &cobra.Command{
Use: "config",
Short: "Output Config Information",
Run: func(cmd *cobra.Command, args []string) {
buf, _ := json.MarshalIndent(app.Conf(), "", " ")
fmt.Println(string(buf))
},
Annotations: map[string]string{
BootModeKey: MODE_MINI_BOOT,
},
}
return configCmd
}
+47 -49
View File
@@ -10,66 +10,64 @@ import (
"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 *core.App) *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
}
+29 -29
View File
@@ -1,41 +1,41 @@
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(app *core.App) *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) {
// change working directory
// if workDir != "" {
// if err := os.Chdir(workDir); err != nil {
// log.Fatal("change working directory error ", err)
// }
// }
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 -24
View File
@@ -7,36 +7,36 @@ import (
"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 *core.App) *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 -71
View File
@@ -2,85 +2,35 @@ 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 *core.App) *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)
if err != nil {
log.Fatalln(err)
}
},
}
flagPV(serverCmd, "host", "", "0.0.0.0", "Listening IP")
flagPV(serverCmd, "port", "", 23366, "Listening port")
return serverCmd
}
+44 -42
View File
@@ -8,61 +8,63 @@ import (
"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(app *core.App) *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) {
// core.LoadConfOnly(cfgFile, workDir)
logrus.Info(i18n.T("Checking for updates") + "...")
log.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.Println("\n-------------------------------\n v" +
latest.Version.String() +
" Release Note\n" +
"-------------------------------\n\n" +
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)
}
+23
View File
@@ -0,0 +1,23 @@
package cmd
import (
"fmt"
"github.com/ArtalkJS/Artalk/internal/core"
"github.com/spf13/cobra"
)
func NewVersionCommand(app *core.App) *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 -1
View File
@@ -1,3 +1,3 @@
{
"latest": "2.5.4"
"latest": "2.6.1"
}
+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
+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.125",
"@redocly/cli": "1.0.2",
"markdown-it-for-inline": "^0.1.1",
"sass": "^1.61.0",
"vitepress": "1.0.0-alpha.65"
"sass": "^1.66.1",
"vitepress": "1.0.0-rc.4"
}
}
+841 -613
View File
File diff suppressed because it is too large. Load diff
+63 -59
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/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/go-testfixtures/testfixtures/v3 v3.8.1
github.com/gofiber/fiber/v2 v2.43.0
github.com/gofiber/swagger v0.1.10
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-20230403015023-b609e27f253c
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.0.3
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.38.1
github.com/sirupsen/logrus v1.9.0
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.12
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.8.0
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/sync v0.1.0
golang.org/x/term v0.7.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,57 +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.4 // 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.18 // 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.21.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/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.27.6 // indirect
github.com/philhofer/fwd v1.1.2 // 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/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.7.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.8.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
)
+167 -236
View File
@@ -1,19 +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/azidentity v1.2.1/go.mod h1:gLa1CL2RNE4s7M3yopJ/p0iq5DdY6Yv5ZUt9MTRZOQM=
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/AzureAD/microsoft-authentication-library-for-go v0.8.1/go.mod h1:4qFor3D/HDsvBME35Xy9rwW9DecL+M2sNw1ybjPtwA0=
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=
@@ -57,27 +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/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
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=
@@ -89,12 +84,12 @@ 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/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=
@@ -112,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=
@@ -121,21 +120,24 @@ 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-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=
@@ -143,22 +145,20 @@ github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf
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.43.0 h1:yit3E4kHf178B60p5CQBa/3v+WVuziWMa/G2ZNyLJB0=
github.com/gofiber/fiber/v2 v2.43.0/go.mod h1:mpS1ZNE5jU+u+BA4FbM+KKnUzJ4wzTK+FT2tG3tU+6I=
github.com/gofiber/swagger v0.1.10 h1:A56mdmITjCjz5jLPctDvGri1kNaKk432ws/RiRXE020=
github.com/gofiber/swagger v0.1.10/go.mod h1:v9qIa0NBsWLwwHkTWwgyvbphsZ0bcbW4zwYtGb7dmY4=
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-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
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=
@@ -173,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=
@@ -199,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=
@@ -242,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=
@@ -262,64 +261,29 @@ github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7V
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
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=
@@ -327,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=
@@ -345,74 +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.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.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU=
github.com/klauspost/compress v1.16.4/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-20230403015023-b609e27f253c h1:dC+1OwE2xOobFVm6+rfuKqk1HUB61Bid/zGRB8Bz5AQ=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230403015023-b609e27f253c/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/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
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.21.0 h1:p2rpHIL7TlSv1QrbXJUAcbyRKnIT0C9rRkH2E4OjLn8=
github.com/microsoft/go-mssqldb v0.21.0/go.mod h1:+4wZTUnz/SV6nffv+RRRB/ss8jPng5Sho2SmM1l2ts4=
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=
@@ -436,12 +394,13 @@ 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=
@@ -454,18 +413,20 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
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=
@@ -481,29 +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.0.3 h1:+7mmR26M0IvyLxGZUHxu4GiBkJkVDid0Un+j4ScYu4k=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
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=
@@ -514,12 +475,8 @@ 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=
@@ -527,25 +484,23 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/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/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/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=
@@ -554,10 +509,10 @@ github.com/steambap/captcha v1.4.1 h1:OmMdxLCWCqJvsFaFYwRpvMckIuvI6s8s1LsBrBw97P
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=
@@ -568,25 +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/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.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w=
github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its=
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=
@@ -596,72 +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.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-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/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
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-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/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.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
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=
@@ -670,7 +613,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
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=
@@ -683,34 +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-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.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0 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.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
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=
@@ -722,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=
@@ -735,7 +678,6 @@ 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=
@@ -763,8 +705,6 @@ 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=
@@ -774,43 +714,39 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0 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.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/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
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=
@@ -822,10 +758,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
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=
@@ -860,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=
@@ -872,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=
@@ -896,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),
})
}
-167
View File
@@ -1,167 +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/sirupsen/logrus"
)
var AntiSpamReplaceKeywords *[]string
const LOG_TAG = "[Spam Interception] "
type SpanCheckConf struct {
ReqReferer string
ReqIP string
ReqUserAgent string
}
func SyncSpamCheck(comment *entity.Comment, conf SpanCheckConf) {
// 拦截评论
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(conf.ReqReferer); 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: conf.ReqIP,
UserAgent: string(conf.ReqUserAgent),
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))
}
+38 -23
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/redis/go-redis/v9"
"github.com/sirupsen/logrus"
)
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.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)
}
-204
View File
@@ -1,204 +0,0 @@
package core
import (
"errors"
"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 _, err := os.Stat(irConf.DBPath); errors.Is(err, os.ErrNotExist) {
logrus.Error("未找到 IP 数据库文件:" + strconv.Quote(irConf.DBPath) + ",IP 归属地功能已禁用," +
"参考链接:https://artalk.js.org/guide/frontend/ip-region.html")
return
}
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))
}
+33
View File
@@ -0,0 +1,33 @@
package core
import "github.com/ArtalkJS/Artalk/internal/anti_spam"
var _ Service = (*AntiSpamService)(nil)
type AntiSpamService struct {
app *App
client *anti_spam.AntiSpam
}
func NewAntiSpamService(app *App) *AntiSpamService {
return &AntiSpamService{app: app}
}
func (s *AntiSpamService) Init() error {
s.client = anti_spam.NewAntiSpam(&anti_spam.AntiSpamConf{
ModeratorConf: s.app.Conf().Moderator,
Dao: s.app.Dao(),
})
return nil
}
func (s *AntiSpamService) Dispose() error {
s.client = nil
return nil
}
func (s *AntiSpamService) CheckAndBlock(data *anti_spam.CheckData) {
s.client.CheckAndBlock(data)
}
+110
View File
@@ -0,0 +1,110 @@
package core
import (
"time"
"github.com/ArtalkJS/Artalk/internal/email"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/log"
)
var _ Service = (*EmailService)(nil)
type EmailService struct {
app *App
queue *email.EmailQueue
}
func NewEmailService(app *App) *EmailService {
return &EmailService{app: app}
}
func (e *EmailService) Init() error {
e.queue = email.NewQueue(email.EmailConf{
EmailConf: e.app.Conf().Email,
OnSendSuccess: func(email *email.Email) {
if email.LinkedNotify == nil {
log.Debug("email.LinkedNotify is null")
return
}
if err := e.app.Dao().NotifySetEmailed(email.LinkedNotify); err != nil { // flag associated comment as emailed
log.Errorf("[Email] Flag email delivery status for associated comment failed: %s", err)
}
},
})
return nil
}
func (e *EmailService) Dispose() error {
if e.queue != nil {
e.queue.Close()
}
return nil
}
func (e *EmailService) AsyncSendTo(subject string, body string, toAddr string) {
if !e.app.Conf().Email.Enabled {
return
}
e.queue.Push(&email.Email{
FromAddr: e.app.Conf().Email.SendAddr,
FromName: e.app.Conf().Email.SendName,
ToAddr: toAddr,
Subject: subject,
Body: body,
})
}
func (e *EmailService) GetRender(useAdminTplParam ...bool) *email.Render {
useAdminTpl := false
if len(useAdminTplParam) > 0 {
useAdminTpl = useAdminTplParam[0]
}
mailTplName := e.app.Conf().Email.MailTpl
// 发送给管理员的邮件单独使用管理员邮件模板
adminTpl := e.app.Conf().AdminNotify.Email.MailTpl
if useAdminTpl && adminTpl != "" {
mailTplName = adminTpl
}
// create new email render instance
render := email.NewRender(e.app.Dao(), mailTplName)
return render
}
func (e *EmailService) AsyncSend(notify *entity.Notify) {
if !e.app.Conf().Email.Enabled {
return
}
receiveUser := e.app.Dao().FetchUserForNotify(notify)
render := e.GetRender(receiveUser.IsAdmin)
// render email body
mailBody := render.RenderEmailBody(notify)
mailSubject := ""
if !receiveUser.IsAdmin {
mailSubject = render.RenderCommon(e.app.Conf().Email.MailSubject, notify)
} else {
mailSubject = render.RenderCommon(e.app.Conf().AdminNotify.Email.MailSubject, notify)
}
log.Debug(time.Now(), " "+receiveUser.Email)
// add email send task to queue
e.queue.Push(&email.Email{
FromAddr: e.app.Conf().Email.SendAddr,
FromName: render.RenderCommon(e.app.Conf().Email.SendName, notify),
ToAddr: receiveUser.Email,
Subject: mailSubject,
Body: mailBody,
LinkedNotify: notify,
})
}
+19
View File
@@ -0,0 +1,19 @@
package core
var _ Service = (*ExampleService)(nil)
type ExampleService struct {
app *App
}
func NewExampleService(app *App) *ExampleService {
return &ExampleService{app: app}
}
func (s *ExampleService) Init() error {
return nil
}
func (s *ExampleService) Dispose() error {
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package core
import "github.com/ArtalkJS/Artalk/internal/ip_region"
var _ Service = (*IPRegionService)(nil)
type IPRegionService struct {
app *App
}
func NewIPRegionService(app *App) *IPRegionService {
return &IPRegionService{app: app}
}
func (s *IPRegionService) Init() error {
return nil
}
func (s *IPRegionService) Dispose() error {
return nil
}
func (s *IPRegionService) Query(ip string) string {
client := ip_region.NewIPRegion(ip_region.IPRegionConf{
IPRegionConf: s.app.Conf().IPRegion,
CacheEnabled: s.app.Conf().Cache.Enabled,
})
return client.IP2Region(ip)
}
+53
View File
@@ -0,0 +1,53 @@
package core
import (
"github.com/ArtalkJS/Artalk/internal/email"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/ArtalkJS/Artalk/internal/notify_pusher"
)
var _ Service = (*NotifyService)(nil)
type NotifyService struct {
app *App
pusher *notify_pusher.NotifyPusher
}
func NewNotifyService(app *App) *NotifyService {
return &NotifyService{app: app}
}
func (s *NotifyService) Init() error {
s.pusher = notify_pusher.NewNotifyPusher(&notify_pusher.NotifyPusherConf{
AdminNotifyConf: s.app.Conf().AdminNotify,
Dao: s.app.Dao(),
EmailPush: func(notify *entity.Notify) error {
emailService, err := AppService[*EmailService](s.app)
if err != nil {
return err
}
emailService.AsyncSend(notify)
return nil
},
EmailRender: func() (*email.Render, error) {
emailService, err := AppService[*EmailService](s.app)
if err != nil {
return nil, err
}
return emailService.GetRender(), nil
},
})
return nil
}
func (s *NotifyService) Dispose() error {
s.pusher = nil
return nil
}
func (s *NotifyService) Push(comment *entity.Comment, pComment *entity.Comment) error {
s.pusher.Push(comment, pComment)
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package dao
import (
"github.com/ArtalkJS/Artalk/internal/entity"
)
// ===============
// Comment
// ===============
func (dao *Dao) FetchUserForComment(c *entity.Comment) entity.User {
return dao.FindUserByID(c.UserID)
}
func (dao *Dao) FetchPageForComment(c *entity.Comment) entity.Page {
return dao.FindPage(c.PageKey, c.SiteName)
}
func (dao *Dao) FetchSiteForComment(c *entity.Comment) entity.Site {
return dao.FindSite(c.SiteName)
}
// ===============
// Page
// ===============
func (dao *Dao) FetchSiteForPage(p *entity.Page) entity.Site {
return dao.FindSite(p.SiteName)
}
// ===============
// Notify
// ===============
func (dao *Dao) FetchCommentForNotify(n *entity.Notify) entity.Comment {
return dao.FindComment(n.CommentID)
}
// 获取接收通知的用户
func (dao *Dao) FetchUserForNotify(n *entity.Notify) entity.User {
return dao.FindUserByID(n.UserID)
}
+80
View File
@@ -0,0 +1,80 @@
package dao_test
import (
"testing"
"github.com/ArtalkJS/Artalk/test"
"github.com/stretchr/testify/assert"
)
func TestFetchUserForComment(t *testing.T) {
app, _ := test.NewTestApp()
defer app.Cleanup()
comment := app.Dao().FindComment(1001)
associatedUser := app.Dao().FetchUserForComment(&comment)
realUser := app.Dao().FindUserByID(1001)
assert.False(t, associatedUser.IsEmpty(), "评论关联用户找不到")
assert.Equal(t, app.Dao().CookUser(&realUser), app.Dao().CookUser(&associatedUser), "评论关联用户数据不一致")
}
func TestFetchPageForComment(t *testing.T) {
app, _ := test.NewTestApp()
defer app.Cleanup()
comment := app.Dao().FindComment(1001)
realPage := app.Dao().FindPage("/test/1000.html", "Site A")
associatedPage := app.Dao().FetchPageForComment(&comment)
assert.False(t, associatedPage.IsEmpty(), "评论关联页面找不到")
assert.Equal(t, app.Dao().CookPage(&realPage), app.Dao().CookPage(&associatedPage), "评论关联页面数据不一致")
}
func TestFetchSiteForComment(t *testing.T) {
app, _ := test.NewTestApp()
defer app.Cleanup()
comment := app.Dao().FindComment(1001)
realSite := app.Dao().FindSite("Site A")
associatedSite := app.Dao().FetchSiteForComment(&comment)
assert.False(t, associatedSite.IsEmpty(), "评论关联站点找不到")
assert.Equal(t, app.Dao().CookSite(&realSite), app.Dao().CookSite(&associatedSite), "评论关联站点数据不一致")
}
func TestFetchSiteForPage(t *testing.T) {
app, _ := test.NewTestApp()
defer app.Cleanup()
page := app.Dao().FindPageByID(1000)
realSite := app.Dao().FindSite("Site A")
associatedSite := app.Dao().FetchSiteForPage(&page)
assert.False(t, associatedSite.IsEmpty(), "页面关联站点找不到")
assert.Equal(t, app.Dao().CookSite(&realSite), app.Dao().CookSite(&associatedSite), "页面关联站点数据不一致")
}
func TestFetchCommentForNotify(t *testing.T) {
app, _ := test.NewTestApp()
defer app.Cleanup()
notify := app.Dao().FindNotify(1000, 1001)
realComment := app.Dao().FindComment(1001)
associatedComment := app.Dao().FetchCommentForNotify(&notify)
assert.False(t, associatedComment.IsEmpty(), "通知关联评论找不到")
assert.Equal(t, app.Dao().CookComment(&realComment), app.Dao().CookComment(&associatedComment), "通知关联评论数据不一致")
}
func TestFetchUserForNotify(t *testing.T) {
app, _ := test.NewTestApp()
defer app.Cleanup()
notify := app.Dao().FindNotify(1000, 1001)
realUser := app.Dao().FindUserByID(1000)
associatedUser := app.Dao().FetchUserForNotify(&notify)
assert.False(t, associatedUser.IsEmpty(), "通知关联用户找不到")
assert.Equal(t, app.Dao().CookUser(&realUser), app.Dao().CookUser(&associatedUser), "通知关联用户数据不一致")
}
+44
View File
@@ -0,0 +1,44 @@
package dao
import (
"gorm.io/gorm"
)
type Dao struct {
db *gorm.DB
// Cache to speed up database query
//
// Please access the cache instance by invoking the `CacheAction` func.
//
// As the cache could be nil.
// Therefore, it is necessary to check if the cache is not nil before referencing it.
cache *DaoCache
}
// Create new dao instance
//
// This function will auto migrate database tables
func NewDao(db *gorm.DB) *Dao {
dao := &Dao{
db: db,
}
dao.MigrateModels()
return dao
}
func (dao *Dao) DB() *gorm.DB {
return dao.db
}
func (dao *Dao) SetCache(cache *DaoCache) {
dao.cache = cache
}
func (dao *Dao) CacheAction(fn func(cache *DaoCache)) {
if dao.cache != nil {
fn(dao.cache)
}
}
+76
View File
@@ -0,0 +1,76 @@
package dao_test
import (
"testing"
"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/stretchr/testify/assert"
"gorm.io/gorm"
)
func newTestDao(t *testing.T) (*dao.Dao, *gorm.DB) {
db, err := db.NewTestDB()
if err != nil {
t.Fatal(err)
}
dao := dao.NewDao(db)
return dao, db
}
func TestBase(t *testing.T) {
dao, ddb := newTestDao(t)
defer db.CloseDB(ddb)
assert.NotNil(t, dao)
}
func TestDB(t *testing.T) {
dao, ddb := newTestDao(t)
defer db.CloseDB(ddb)
assert.Equal(t, ddb, dao.DB())
}
func newTestCache(t *testing.T) *cache.Cache {
c, err := cache.New(config.CacheConf{
Type: config.CacheTypeBuiltin,
})
if err != nil {
t.Fatal(err)
}
return c
}
func TestDaoCache(t *testing.T) {
d, _ := newTestDao(t)
cacheAdaptor := dao.NewCacheAdaptor(newTestCache(t))
defer cacheAdaptor.Close()
t.Run("SetCache", func(t *testing.T) {
d.SetCache(cacheAdaptor)
})
t.Run("CacheAction_Valid", func(t *testing.T) {
cacheActionFnInvoked := false
d.CacheAction(func(cache *dao.DaoCache) {
cacheActionFnInvoked = true
assert.Equal(t, cacheAdaptor, cache)
})
assert.True(t, cacheActionFnInvoked)
})
t.Run("CacheAction_Invalid", func(t *testing.T) {
d, _ := newTestDao(t)
cacheActionFnInvoked := false
d.CacheAction(func(cache *dao.DaoCache) {
cacheActionFnInvoked = true
})
assert.False(t, cacheActionFnInvoked)
})
}
+128
View File
@@ -0,0 +1,128 @@
package dao
import (
"fmt"
"strings"
"github.com/ArtalkJS/Artalk/internal/cache"
"github.com/ArtalkJS/Artalk/internal/entity"
"golang.org/x/exp/slices"
)
const (
UserByIDKey = "user#id=%d"
UserByNameEmailKey = "user#name=%s;email=%s"
UserIDByEmailKey = "user_id#email=%s"
SiteByIDKey = "site#id=%d"
SiteByNameKey = "site#name=%s"
PageByIDKey = "page#id=%d"
PageByKeySiteNameKey = "page#key=%s;site_name=%s"
CommentByIDKey = "comment#id=%d"
CommentChildIDsByIDKey = "comment_child_ids#id=%d"
)
type DaoCache struct {
*cache.Cache
}
func NewCacheAdaptor(cache *cache.Cache) *DaoCache {
return &DaoCache{Cache: cache}
}
func (c *DaoCache) UserCacheSave(user *entity.User) error {
return c.StoreCache(user,
fmt.Sprintf(UserByIDKey, user.ID),
fmt.Sprintf(UserByNameEmailKey, strings.ToLower(user.Name), strings.ToLower(user.Email)),
)
}
func (c *DaoCache) UserCacheDel(user *entity.User) {
c.DelCache(
fmt.Sprintf(UserByIDKey, user.ID),
fmt.Sprintf(UserByNameEmailKey, strings.ToLower(user.Name), strings.ToLower(user.Email)),
)
}
func (c *DaoCache) SiteCacheSave(site *entity.Site) error {
return c.StoreCache(site,
fmt.Sprintf(SiteByIDKey, site.ID),
fmt.Sprintf(SiteByNameKey, site.Name),
)
}
func (c *DaoCache) SiteCacheDel(site *entity.Site) {
c.DelCache(fmt.Sprintf(SiteByIDKey, site.ID))
c.DelCache(fmt.Sprintf(SiteByNameKey, site.Name))
}
func (c *DaoCache) PageCacheSave(page *entity.Page) error {
return c.StoreCache(page,
fmt.Sprintf(PageByIDKey, page.ID),
fmt.Sprintf(PageByKeySiteNameKey, page.Key, page.SiteName),
)
}
func (c *DaoCache) PageCacheDel(page *entity.Page) {
c.DelCache(
fmt.Sprintf(PageByIDKey, page.ID),
fmt.Sprintf(PageByKeySiteNameKey, page.Key, page.SiteName),
)
}
func (c *DaoCache) CommentCacheSave(comment *entity.Comment) (err error) {
if storeErr := c.StoreCache(comment, fmt.Sprintf(CommentByIDKey, comment.ID)); storeErr != nil {
err = storeErr
}
if storeErr := c.ChildCommentCacheSave(comment); storeErr != nil {
err = storeErr
}
return
}
func (c *DaoCache) CommentCacheDel(comment *entity.Comment) {
c.DelCache(fmt.Sprintf(CommentByIDKey, comment.ID))
c.ChildCommentCacheDel(comment)
}
// 缓存 父ID=>子ID 评论数据
func (c *DaoCache) ChildCommentCacheSave(comment *entity.Comment) error {
// 若 comment 为根评论
if comment.Rid == 0 {
// 查询是否有缓存数据
// 若无缓存数据,则创建初始空数据,无子评论
// 若有缓存数据,说明 comment 是 update 操作,则不更改
cacheName := fmt.Sprintf(CommentChildIDsByIDKey, comment.ID)
var childIDs []uint
if err := c.FindCache(cacheName, &childIDs); err != nil {
// 无缓存,则初始化
return c.StoreCache([]uint{}, cacheName)
}
return nil
}
// 若 comment 为子评论(Rid 不为空,Rid 为父 ID),
// 则为父评论的 “子评论 ID 列表” 追加 “子评论 ID”
parentID := comment.Rid
childID := comment.ID
var cacheName = fmt.Sprintf(CommentChildIDsByIDKey, parentID)
var childIDs []uint
c.FindCache(cacheName, &childIDs)
// append if childID Not contains
if !slices.Contains(childIDs, childID) {
childIDs = append(childIDs, childID)
}
return c.StoreCache(childIDs, cacheName)
}
func (c *DaoCache) ChildCommentCacheDel(comment *entity.Comment) {
c.DelCache(fmt.Sprintf(CommentChildIDsByIDKey, comment.ID))
if comment.Rid != 0 {
c.DelCache(fmt.Sprintf(CommentChildIDsByIDKey, comment.Rid))
// TODO 从 slice 中 remove 并更新缓存而不是直接删除缓存,删除缓存会导致重新查询 db 而性能下降
}
}
+213
View File
@@ -0,0 +1,213 @@
package dao_test
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/ArtalkJS/Artalk/internal/dao"
"github.com/ArtalkJS/Artalk/internal/entity"
"github.com/stretchr/testify/assert"
"gorm.io/gorm"
)
func newTestDaoCache(t *testing.T) *dao.DaoCache {
cache := newTestCache(t)
return dao.NewCacheAdaptor(cache)
}
func TestNewCacheAdaptor(t *testing.T) {
cache := newTestDaoCache(t)
defer cache.Close()
assert.NotNil(t, cache)
}
func TestCache(t *testing.T) {
user := entity.User{
Model: gorm.Model{
ID: 233,
},
Name: "Artalk",
Email: "artalkjs@gmail.com",
Password: "12345",
}
site := entity.Site{
Model: gorm.Model{
ID: 123,
},
Name: "2333",
}
page := entity.Page{
Model: gorm.Model{
ID: 343,
},
Key: "/links.html",
Title: "Hello World",
SiteName: site.Name,
}
comment := entity.Comment{
Model: gorm.Model{
ID: 2321,
},
Content: "abcdefg",
Rid: 0,
UserID: user.ID,
PageKey: page.Key,
SiteName: page.SiteName,
}
childComment := entity.Comment{
Model: gorm.Model{
ID: 777,
},
Content: "233",
Rid: comment.ID,
UserID: user.ID,
PageKey: page.Key,
SiteName: page.SiteName,
}
tests := []struct {
name string
data any
saveFunc func(cache *dao.DaoCache) error
keys []string
delFunc func(cache *dao.DaoCache) error
}{
// User Cache Test
{
data: &user,
saveFunc: func(cache *dao.DaoCache) error {
return cache.UserCacheSave(&user)
},
keys: []string{
fmt.Sprintf(dao.UserByIDKey, user.ID),
fmt.Sprintf(dao.UserByNameEmailKey, strings.ToLower(user.Name), strings.ToLower(user.Email)),
},
delFunc: func(cache *dao.DaoCache) error {
cache.UserCacheDel(&user)
return nil
},
},
// Site Cache Test
{
data: &site,
saveFunc: func(cache *dao.DaoCache) error {
return cache.SiteCacheSave(&site)
},
keys: []string{
fmt.Sprintf(dao.SiteByIDKey, site.ID),
fmt.Sprintf(dao.SiteByNameKey, site.Name),
},
delFunc: func(cache *dao.DaoCache) error {
cache.SiteCacheDel(&site)
return nil
},
},
// Page Cache Test
{
data: &page,
saveFunc: func(cache *dao.DaoCache) error {
return cache.PageCacheSave(&page)
},
keys: []string{
fmt.Sprintf(dao.PageByIDKey, page.ID),
},
delFunc: func(cache *dao.DaoCache) error {
cache.PageCacheDel(&page)
return nil
},
},
// Comment Cache Test
{
data: &comment,
saveFunc: func(cache *dao.DaoCache) error {
return cache.CommentCacheSave(&comment)
},
keys: []string{
fmt.Sprintf(dao.CommentByIDKey, comment.ID),
},
delFunc: func(cache *dao.DaoCache) error {
cache.CommentCacheDel(&comment)
return nil
},
},
// Child Comment Cache Test
{
data: &childComment,
saveFunc: func(cache *dao.DaoCache) error {
return cache.CommentCacheSave(&childComment)
},
keys: []string{
fmt.Sprintf(dao.CommentByIDKey, childComment.ID),
},
delFunc: func(cache *dao.DaoCache) error {
cache.CommentCacheDel(&childComment)
return nil
},
},
}
for _, tt := range tests {
for _, key := range tt.keys {
t.Run(fmt.Sprintf("%T", tt.data)+"/Key="+key, func(t *testing.T) {
// run test for each key
cache := newTestDaoCache(t)
defer cache.Close()
// func to let Marshaller know the type of the `tt.data`,
// rather than use `var data any`
createEmptyTypedData := func() any {
p := reflect.ValueOf(tt.data).Elem()
return reflect.New(p.Type()).Interface()
}
// Check if key exists before save
t.Run("FindBeforeSave", func(t *testing.T) {
var data any
err := cache.FindCache(key, &data)
assert.Error(t, err) // Key should not exist before save
assert.Empty(t, data)
})
// save cache
t.Run("Save", func(t *testing.T) {
err := tt.saveFunc(cache)
assert.NoError(t, err)
})
// Check if key exists after save
t.Run("FindAfterSave", func(t *testing.T) {
data := createEmptyTypedData()
err := cache.FindCache(key, &data)
if assert.NoError(t, err) {
assert.EqualValues(t, tt.data, data) // data should equal after cache save
}
})
// Check if key exists after delete
t.Run("DeleteAfterSave", func(t *testing.T) {
err := tt.delFunc(cache)
assert.NoError(t, err)
})
t.Run("FindAfterDelete", func(t *testing.T) {
var data any
err := cache.FindCache(key, &data)
assert.Error(t, err)
assert.Empty(t, data)
})
})
}
}
}
Loaded 100 of 253 files, more files were not shown because too many files have changed in this diff. Show more