Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0892fc4c0 | ||
|
|
c1e365fdfd | ||
|
|
e6e6d2aa54 | ||
|
|
ac70cd4c9d | ||
|
|
600030b528 | ||
|
|
4e1537b4f0 | ||
|
|
8a8aac945d | ||
|
|
89be748dee | ||
|
|
6c35fe9ded | ||
|
|
9ea6e8d981 | ||
|
|
2c89c2d633 | ||
|
|
5eddd9ff05 | ||
|
|
9fdfb2a043 | ||
|
|
2bdb806aaf | ||
|
|
a1b3f1a1f7 | ||
|
|
dc333e5a9a | ||
|
|
f0ecff9199 | ||
|
|
e3ccc655c5 | ||
|
|
04a96d4ab8 | ||
|
|
71e0fcec75 | ||
|
|
963d3cf3d9 | ||
|
|
2669b0efe8 | ||
|
|
a8b4eb6047 | ||
|
|
cc7b2f574c | ||
|
|
150de6d7c0 | ||
|
|
1da3f6420e | ||
|
|
919dfa43d0 | ||
|
|
b6805ce948 | ||
|
|
137610aa82 | ||
|
|
0cbb5b2dd1 | ||
|
|
9e59fefc8a | ||
|
|
6ead92db54 | ||
|
|
388b5b8b24 | ||
|
|
e15e22ecda | ||
|
|
8336bb0345 | ||
|
|
fc6c6286db | ||
|
|
3af4482db6 | ||
|
|
c9f8e9b824 | ||
|
|
d9c2813e50 | ||
|
|
84d6f9728e | ||
|
|
d1b730113b | ||
|
|
731be57e74 | ||
|
|
9f7fb9dad2 | ||
|
|
9f81879713 | ||
|
|
ff8527f007 | ||
|
|
2e07d2a2ca | ||
|
|
8f5a84969b | ||
|
|
1e76a4d99f | ||
|
|
840621a6b1 | ||
|
|
24485f9615 | ||
|
|
d4f0ae8ad7 | ||
|
|
a06928378a | ||
|
|
bf93ca78c8 | ||
|
|
75180877b1 | ||
|
|
fb4aaf7ecb | ||
|
|
d58355209a | ||
|
|
8488b7e18b | ||
|
|
e7aced94dd | ||
|
|
3dd93c33b3 | ||
|
|
bd613a3304 | ||
|
|
7028dcd905 | ||
|
|
9ef063cf98 | ||
|
|
bf13e1cdba | ||
|
|
79f5f53886 | ||
|
|
4c951d7e44 | ||
|
|
b034e514a7 | ||
|
|
b88a26dcae | ||
|
|
ee30ba4fc8 | ||
|
|
a02b89ba3e | ||
|
|
2ca3ab8274 | ||
|
|
cbf0210131 | ||
|
|
d8d9420616 | ||
|
|
891f792fe4 | ||
|
|
d538e47917 | ||
|
|
6c7ce98522 | ||
|
|
1e839548a4 | ||
|
|
4ea45ae375 | ||
|
|
91f89c6b7d | ||
|
|
0ac70a5a43 | ||
|
|
9ac354cb23 | ||
|
|
40e6fcd966 | ||
|
|
7e476ded1c | ||
|
|
a885c0a179 | ||
|
|
08c0b297e5 | ||
|
|
01ed04c791 | ||
|
|
0135a83b49 | ||
|
|
b9c177bb54 | ||
|
|
00ce23bc99 | ||
|
|
4853b6cc5d | ||
|
|
79dea425ea | ||
|
|
c20faed4e3 | ||
|
|
e9acd28126 | ||
|
|
b71b22f5a7 | ||
|
|
bc7d0e09b8 | ||
|
|
f682ade2c9 | ||
|
|
2b990ff433 | ||
|
|
6d21c2b074 | ||
|
|
3840720620 | ||
|
|
7dd8668b6e | ||
|
|
ac4b8b0b80 |
No files matched your search
@@ -9,10 +9,10 @@ executors:
|
||||
- image: cimg/base:stable
|
||||
node:
|
||||
docker:
|
||||
- image: 'cimg/node:16.15.1'
|
||||
- image: 'cimg/node:18.17.1'
|
||||
go:
|
||||
docker:
|
||||
- image: cimg/go:1.20
|
||||
- image: cimg/go:1.21.0
|
||||
|
||||
parameters:
|
||||
trigger-app:
|
||||
@@ -40,12 +40,12 @@ jobs:
|
||||
go version
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-mod-1.20-{{ checksum "go.sum" }}
|
||||
- go-mod-1.21.0-{{ checksum "go.sum" }}
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: go mod download
|
||||
- save_cache:
|
||||
key: go-mod-1.20-{{ checksum "go.sum" }}
|
||||
key: go-mod-1.21.0-{{ checksum "go.sum" }}
|
||||
paths:
|
||||
- "~/go/pkg/mod"
|
||||
- run:
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
command: node -v
|
||||
- run:
|
||||
name: Install pnpm
|
||||
command: npm i -g pnpm@7.25.0
|
||||
command: npm i -g pnpm@8.7.0
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: pnpm --dir ./ui install --frozen-lockfile
|
||||
|
||||
@@ -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.
@@ -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
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": ["config:base", "schedule:weekly"],
|
||||
"automerge": false,
|
||||
"major": {
|
||||
"automerge": false
|
||||
},
|
||||
"branchPrefix": "chore/",
|
||||
"labels": ["dependencies"],
|
||||
"gomodTidy": true,
|
||||
"rangeStrategy": "bump",
|
||||
"reviewers": []
|
||||
}
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
types: [artalk-release]
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.20.2'
|
||||
GO_VERSION: '1.21.0'
|
||||
PKG_NAME: 'github.com/ArtalkJS/Artalk'
|
||||
DOCKER_IMG: ghcr.io/goreleaser/goreleaser-cross
|
||||
CACHE_DIR: /tmp/cache/docker-image
|
||||
@@ -30,23 +30,25 @@ jobs:
|
||||
|
||||
echo "VERSION=${VERSION}" >> $GITHUB_ENV
|
||||
|
||||
- name: Docker Image Cache
|
||||
id: docker-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ${{ env.CACHE_DIR }}
|
||||
key: docker-image-go-cross-${{ env.GO_VERSION }}
|
||||
# disable cache because the err `no space left on device`
|
||||
# - name: Docker Image Cache
|
||||
# id: docker-cache
|
||||
# uses: actions/cache@v3
|
||||
# with:
|
||||
# path: ${{ env.CACHE_DIR }}
|
||||
# key: docker-image-go-cross-${{ env.GO_VERSION }}
|
||||
|
||||
- name: Pull Docker Image
|
||||
if: steps.docker-cache.outputs.cache-hit != 'true'
|
||||
# if: steps.docker-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
docker pull "${DOCKER_IMG}:v${GO_VERSION}"
|
||||
mkdir -p "${CACHE_DIR}"
|
||||
docker save -o "${CACHE_DIR}/go-cross.tar" "${DOCKER_IMG}:v${GO_VERSION}"
|
||||
|
||||
- name: Restore Docker Image from Cache
|
||||
if: steps.docker-cache.outputs.cache-hit == 'true'
|
||||
run: docker load -i ${CACHE_DIR}/go-cross.tar
|
||||
# mkdir -p "${CACHE_DIR}"
|
||||
# docker save -o "${CACHE_DIR}/go-cross.tar" "${DOCKER_IMG}:v${GO_VERSION}"
|
||||
|
||||
# - name: Restore Docker Image from Cache
|
||||
# if: steps.docker-cache.outputs.cache-hit == 'true'
|
||||
# run: docker load -i ${CACHE_DIR}/go-cross.tar
|
||||
|
||||
- name: Setup git-chglog
|
||||
run: |
|
||||
@@ -62,7 +64,7 @@ jobs:
|
||||
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" > local/.release-env
|
||||
|
||||
# changelog
|
||||
git-chglog ${VERSION} > local/release-notes.md
|
||||
git-chglog --config .github/chglog/config.yml ${VERSION} > local/release-notes.md
|
||||
|
||||
# copy config file
|
||||
cp conf/artalk.example.yml artalk.yml
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,12 +25,12 @@ jobs:
|
||||
- name: Use pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 18.x
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: './ui/pnpm-lock.yaml'
|
||||
|
||||
@@ -35,12 +35,12 @@ jobs:
|
||||
- name: Use pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 18.x
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: 'pnpm'
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 18.x
|
||||
registry-url: https://registry.npmjs.org/
|
||||
|
||||
- name: Handle Version Number
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
run: |
|
||||
if [ ! $(git tag -l "${VERSION}") ]; then
|
||||
# prepend changelog
|
||||
changelog=$(git-chglog --next-tag "${VERSION}" "${VERSION}")
|
||||
changelog=$(git-chglog --config .github/chglog/config.yml --next-tag "${VERSION}" "${VERSION}")
|
||||
echo -e "${changelog}\n" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md
|
||||
|
||||
# modify the version of `package.json`
|
||||
|
||||
@@ -48,6 +48,7 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
__debug*
|
||||
|
||||
# frontend
|
||||
node_modules
|
||||
|
||||
Vendored
+2
-2
@@ -8,7 +8,7 @@
|
||||
"mode": "exec",
|
||||
"program": "${workspaceFolder}/bin/artalk",
|
||||
"preLaunchTask": "Build for debugging",
|
||||
"args": ["server", "-c", "${workspaceFolder}/local/local.yml"]
|
||||
"args": ["server", "-c", "${workspaceFolder}/data/artalk.yml"]
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
<p align="center">
|
||||
<img src="./docs/artalk.png" alt="Artalk" width="100%">
|
||||
</p>
|
||||
|
||||
# Artalk
|
||||
|
||||
[](https://circleci.com/gh/ArtalkJS/Artalk/tree/master)
|
||||
[](https://github.com/ArtalkJS/Artalk/actions/workflows/dockerhub.yml) [](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_shield)
|
||||
[](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
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_large)
|
||||
+132
@@ -1,4 +1,136 @@
|
||||
|
||||
## [v2.6.2](https://github.com/ArtalkJS/Artalk/compare/v2.6.1...v2.6.2) (2023-09-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cmd:** remove app instance creation when mini boot ([#577](https://github.com/ArtalkJS/Artalk/issues/577))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **style:** modify `z-index` less than 5 ([#578](https://github.com/ArtalkJS/Artalk/issues/578))
|
||||
|
||||
|
||||
## [v2.6.1](https://github.com/ArtalkJS/Artalk/compare/v2.6.0...v2.6.1) (2023-09-11)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **captcha:** image captcha base64 response issue ([#575](https://github.com/ArtalkJS/Artalk/issues/575))
|
||||
|
||||
### Documentation
|
||||
|
||||
* **config:** add info about env variables ([#566](https://github.com/ArtalkJS/Artalk/issues/566))
|
||||
* **extras:** add deploy guide for railway.app ([#567](https://github.com/ArtalkJS/Artalk/issues/567))
|
||||
|
||||
|
||||
## [v2.6.0](https://github.com/ArtalkJS/Artalk/compare/v2.5.5...v2.6.0) (2023-09-01)
|
||||
|
||||
### Features
|
||||
|
||||
* **config:** support load env variables as config ([#564](https://github.com/ArtalkJS/Artalk/issues/564))
|
||||
* **go:** upgrade golang to v1.21.0 and some deps ([#543](https://github.com/ArtalkJS/Artalk/issues/543))
|
||||
* **hook:** add hook pkg
|
||||
* **simple_cache:** a simple thread-safe cache with sync.Map
|
||||
* **utils:** add common random utils
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cache:** child comment ids store after comment updated
|
||||
* **cmd:** fix parse root cmd global flags
|
||||
* **config:** fix ip region patch
|
||||
* **db:** fix dsn strings for mysql connections ([#541](https://github.com/ArtalkJS/Artalk/issues/541))
|
||||
* **limiter:** limiter log ignore options method req
|
||||
* **limiter:** fix limiter always mode
|
||||
* **log:** add missing log for web server listen func ([#556](https://github.com/ArtalkJS/Artalk/issues/556))
|
||||
* **ui:** fix plug cannot disable issue ([#563](https://github.com/ArtalkJS/Artalk/issues/563))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **cmd:** support command mini boot mode
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **CI:** replace using `docker/metadata-action` for the docker push ([#545](https://github.com/ArtalkJS/Artalk/issues/545))
|
||||
* **CI:** replace renovate with github dependabot ([#546](https://github.com/ArtalkJS/Artalk/issues/546))
|
||||
* **anti_spam:** refactor anti_spam pkg
|
||||
* **artransfer:** refactor artransfer pkg
|
||||
* **cache:** remove cached fields in struct to save memory ([#555](https://github.com/ArtalkJS/Artalk/issues/555))
|
||||
* **cache:** cache code tidy-up
|
||||
* **cache:** refactor cache pkg
|
||||
* **cache:** refactor cache pkg
|
||||
* **cache:** refactor cache pkg
|
||||
* **captcha:** refactor captcha pkg
|
||||
* **cloud:** refactor cloud pkg
|
||||
* **cmd:** refactor cmd pkg
|
||||
* **conf:** better config.NewFromFile with error handle
|
||||
* **config:** refactor config pkg
|
||||
* **core:** refactor core pkg
|
||||
* **core:** better AppService func with error handle
|
||||
* **cors:** refactor cors middleware
|
||||
* **dao:** refactor dao pkg
|
||||
* **dao_cache:** add dao_cache pkg
|
||||
* **db:** refactor db pkg
|
||||
* **email:** refactor email pkg
|
||||
* **i18n:** refactor i18n pkg
|
||||
* **ip_region:** refactor ip_region pkg
|
||||
* **limiter:** add limiter pkg
|
||||
* **log:** create common log pkg
|
||||
* **main:** refactor main.go
|
||||
* **notify:** add notify_pusher pkg
|
||||
* **server/common:** refactor server/common pkg
|
||||
* **server/handler:** refactor server/handler pkg
|
||||
* **server/middleware:** refactor server/middleware pkg
|
||||
* **test:** create common test pkg
|
||||
|
||||
### Documentation
|
||||
|
||||
* **extras:** add deploy guide for fly.io ([#520](https://github.com/ArtalkJS/Artalk/issues/520))
|
||||
|
||||
|
||||
## [v2.5.5](https://github.com/ArtalkJS/Artalk/compare/v2.5.4...v2.5.5) (2023-05-10)
|
||||
|
||||
### Features
|
||||
|
||||
* **build:** add embedded fe lite and i18n scripts ([#493](https://github.com/ArtalkJS/Artalk/issues/493)) ([#505](https://github.com/ArtalkJS/Artalk/issues/505))
|
||||
* **go:** upgrade golang to v1.20.4 and some deps ([#506](https://github.com/ArtalkJS/Artalk/issues/506))
|
||||
* **gravatar:** add `gravatar.params` config option ([#508](https://github.com/ArtalkJS/Artalk/issues/508))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** comment IP data loss after editing ([#504](https://github.com/ArtalkJS/Artalk/issues/504))
|
||||
* **editor:** prevent editor reset on comment failure ([#507](https://github.com/ArtalkJS/Artalk/issues/507))
|
||||
|
||||
### BREAKING CHANGE
|
||||
|
||||
|
||||
The `gravatar.default` config option has been removed. Please use `gravatar.params` instead. The default value for `gravatar.params` is now `"d=mp&s=240"`.
|
||||
|
||||
|
||||
## [v2.5.4](https://github.com/ArtalkJS/Artalk/compare/v2.5.3...v2.5.4) (2023-04-09)
|
||||
|
||||
### Features
|
||||
|
||||
* **go:** upgrade golang to v1.20.3 and some deps ([#485](https://github.com/ArtalkJS/Artalk/issues/485))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **CI:** upgrade and pin node.js and pnpm version ([#484](https://github.com/ArtalkJS/Artalk/issues/484))
|
||||
|
||||
### Documentation
|
||||
|
||||
* update swagger docs and some deps
|
||||
|
||||
|
||||
## [v2.5.3](https://github.com/ArtalkJS/Artalk/compare/v2.5.2...v2.5.3) (2023-04-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api/comment_add:** SIGSEGV crash in comment_add ([#478](https://github.com/ArtalkJS/Artalk/issues/478)) ([#481](https://github.com/ArtalkJS/Artalk/issues/481))
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **ip_region:** add db file existence check and error message ([#482](https://github.com/ArtalkJS/Artalk/issues/482))
|
||||
|
||||
|
||||
## [v2.5.2](https://github.com/ArtalkJS/Artalk/compare/v2.5.1...v2.5.2) (2023-03-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
+4
-4
@@ -7,7 +7,7 @@ This guide is for developers who want to contribute to the project.
|
||||
To develop Artalk, including the frontend and backend, you need to install the following tools:
|
||||
|
||||
- [Node.js](https://nodejs.org/en/) (>= 16.0.0)
|
||||
- [Go](https://golang.org/) (>= 1.20)
|
||||
- [Go](https://golang.org/) (>= 1.21)
|
||||
- [Docker](https://www.docker.com/) (>= 20.10.0) (optional)
|
||||
- [Docker Compose](https://docs.docker.com/compose/) (>= 1.29.0) (optional)
|
||||
|
||||
@@ -50,12 +50,12 @@ If you want to run a demo site, you can use the following command:
|
||||
./scripts/setup-example-site.sh
|
||||
```
|
||||
|
||||
This script sets up a local example site for testing at `local` folder, with Artalk integrated into its theme.
|
||||
This script sets up a local example site for testing at `data` folder, with Artalk integrated into its theme.
|
||||
|
||||
After running this script, run:
|
||||
|
||||
```sh
|
||||
./bin/artalk server -c ./local/local.yml
|
||||
./bin/artalk server -c ./data/artalk.yml
|
||||
```
|
||||
|
||||
to start the artalk server.
|
||||
@@ -80,7 +80,7 @@ Artalk is a monorepo project, which means all the source code is in the same rep
|
||||
- `docs/` - The documentation site source code.
|
||||
- `i18n/` - The translation files.
|
||||
- `internal/` - The internal packages.
|
||||
- `local/` - The local example site. This directory is ignored by git.
|
||||
- `data/` - The local data. This directory is ignored by git.
|
||||
- `public/` - The static files. Built frontend files will be copied here.
|
||||
- `scripts/` - The scripts for development.
|
||||
- `server/` - The source code for the server.
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
### build Artalk
|
||||
FROM golang:1.20.2-alpine3.17 as builder
|
||||
FROM golang:1.21.0-alpine3.18 as builder
|
||||
|
||||
WORKDIR /source
|
||||
|
||||
# install tools
|
||||
RUN set -ex \
|
||||
&& apk add --no-cache make git gcc musl-dev nodejs bash npm\
|
||||
&& npm install -g pnpm@7.25.0
|
||||
&& npm install -g pnpm@8.7.0
|
||||
|
||||
# download go deps
|
||||
# (cache by separating the downloading of deps)
|
||||
@@ -23,7 +23,7 @@ RUN set -ex \
|
||||
&& make build
|
||||
|
||||
### build final image
|
||||
FROM alpine:3.17
|
||||
FROM alpine:3.18
|
||||
|
||||
# we set the timezone `Asia/Shanghai` by default, you can be modified
|
||||
# by `docker build --build-arg="TZ=Other_Timezone ..."`
|
||||
|
||||
@@ -1,165 +1,21 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
Copyright (c) 2018-present, qwqcode and other contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -44,6 +44,10 @@ test:
|
||||
test-coverage:
|
||||
$(GOTEST) -cover ./...
|
||||
|
||||
test-coverage-html:
|
||||
$(GOTEST) -v -coverprofile=coverage.out ./...
|
||||
go tool cover -html=coverage.out
|
||||
|
||||
update-i18n:
|
||||
go generate ./internal/i18n
|
||||
|
||||
@@ -57,5 +61,5 @@ docker-push:
|
||||
./scripts/docker-build.sh --push
|
||||
|
||||
.PHONY: all install build build-frontend build-debug \
|
||||
dev test test-coverage update-i18n \
|
||||
dev test test-coverage test-coverage-html update-i18n \
|
||||
docker-build docker-push;
|
||||
+1
-1
@@ -131,7 +131,7 @@ Thanks for the help and feedback provided by the community, if you have good sug
|
||||
|
||||
## License
|
||||
|
||||
[LGPL-3.0](./LICENSE)
|
||||
[MIT](./LICENSE)
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_shield)
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ see [CONTRIBUTING.md](./CONTRIBUTING.md)
|
||||
|
||||
## License
|
||||
|
||||
[LGPL-3.0](./LICENSE)
|
||||
[MIT](./LICENSE)
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_shield)
|
||||
|
||||
|
||||
+44
-49
@@ -2,72 +2,67 @@ package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/core"
|
||||
"github.com/ArtalkJS/Artalk/internal/entity"
|
||||
"github.com/ArtalkJS/Artalk/internal/i18n"
|
||||
"github.com/ArtalkJS/Artalk/internal/query"
|
||||
"github.com/ArtalkJS/Artalk/internal/log"
|
||||
"github.com/ArtalkJS/Artalk/internal/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var adminCmd = &cobra.Command{
|
||||
Use: "admin",
|
||||
Short: "Create or edit an administrator account",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
core.LoadCore(cfgFile, workDir)
|
||||
func NewAdminCommand(app *ArtalkCmd) *cobra.Command {
|
||||
adminCmd := &cobra.Command{
|
||||
Use: "admin",
|
||||
Short: "Create or edit an administrator account",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("--------------------------------")
|
||||
fmt.Println(" " + i18n.T("Create admin account"))
|
||||
fmt.Println("--------------------------------")
|
||||
|
||||
fmt.Println("--------------------------------")
|
||||
fmt.Println(" " + i18n.T("Create admin account"))
|
||||
fmt.Println("--------------------------------")
|
||||
|
||||
username, email, password, err := credentials()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
findUser := query.FindUser(username, email)
|
||||
if !findUser.IsEmpty() {
|
||||
findUser.SetPasswordEncrypt(password)
|
||||
if err := query.UpdateUser(&findUser); err != nil {
|
||||
logrus.Fatal(err)
|
||||
username, email, password, err := credentials()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Info(i18n.T("{{name}} already exists", map[string]interface{}{"name": i18n.T("Account")}) +
|
||||
", " + i18n.T("Password updated"))
|
||||
return
|
||||
}
|
||||
findUser := app.Dao().FindUser(username, email)
|
||||
if !findUser.IsEmpty() {
|
||||
findUser.SetPasswordEncrypt(password)
|
||||
if err := app.Dao().UpdateUser(&findUser); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
user := entity.User{
|
||||
Name: username,
|
||||
Email: email,
|
||||
IsAdmin: true,
|
||||
BadgeName: i18n.T("Admin"),
|
||||
BadgeColor: "#0083FF",
|
||||
}
|
||||
user.SetPasswordEncrypt(password)
|
||||
log.Info(i18n.T("{{name}} already exists", map[string]interface{}{"name": i18n.T("Account")}) +
|
||||
", " + i18n.T("Password updated"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := query.CreateUser(&user); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
user := entity.User{
|
||||
Name: username,
|
||||
Email: email,
|
||||
IsAdmin: true,
|
||||
BadgeName: i18n.T("Admin"),
|
||||
BadgeColor: "#0083FF",
|
||||
}
|
||||
user.SetPasswordEncrypt(password)
|
||||
|
||||
fmt.Println("--------------------------------")
|
||||
fmt.Println(" Name: " + username)
|
||||
fmt.Println(" Mail: " + email)
|
||||
fmt.Println("--------------------------------")
|
||||
},
|
||||
}
|
||||
if err := app.Dao().CreateUser(&user); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(adminCmd)
|
||||
fmt.Println("--------------------------------")
|
||||
fmt.Println(" Name: " + username)
|
||||
fmt.Println(" Mail: " + email)
|
||||
fmt.Println("--------------------------------")
|
||||
},
|
||||
}
|
||||
|
||||
return adminCmd
|
||||
}
|
||||
|
||||
func credentials() (string, string, string, error) {
|
||||
@@ -85,7 +80,7 @@ func credentials() (string, string, string, error) {
|
||||
return "", "", "", err
|
||||
}
|
||||
if !utils.ValidateEmail(strings.TrimSpace(email)) {
|
||||
return "", "", "", errors.New("invalid email format")
|
||||
return "", "", "", fmt.Errorf("invalid email format")
|
||||
}
|
||||
|
||||
fmt.Print(i18n.T("Enter {{name}}", map[string]interface{}{"name": i18n.T("Password")}) + ": ")
|
||||
@@ -107,7 +102,7 @@ func credentials() (string, string, string, error) {
|
||||
rePassword := strings.TrimSpace(string(byteRePassword))
|
||||
|
||||
if rePassword != password {
|
||||
return "", "", "", errors.New("inconsistent password input")
|
||||
return "", "", "", fmt.Errorf("inconsistent password input")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(username), strings.TrimSpace(email), password, nil
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/config"
|
||||
"github.com/ArtalkJS/Artalk/internal/core"
|
||||
"github.com/ArtalkJS/Artalk/internal/log"
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Version = config.Version + `/` + config.CommitHash
|
||||
|
||||
var Banner = `
|
||||
________ ________ _________ ________ ___ ___ __
|
||||
|\ __ \|\ __ \|\___ ___\\ __ \|\ \ |\ \|\ \
|
||||
\ \ \|\ \ \ \|\ \|___ \ \_\ \ \|\ \ \ \ \ \ \/ /|_
|
||||
\ \ __ \ \ _ _\ \ \ \ \ \ __ \ \ \ \ \ ___ \
|
||||
\ \ \ \ \ \ \\ \| \ \ \ \ \ \ \ \ \ \____\ \ \\ \ \
|
||||
\ \__\ \__\ \__\\ _\ \ \__\ \ \__\ \__\ \_______\ \__\\ \__\
|
||||
\|__|\|__|\|__|\|__| \|__| \|__|\|__|\|_______|\|__| \|__|
|
||||
|
||||
Artalk (` + Version + `)
|
||||
|
||||
-> A Selfhosted Comment System.
|
||||
-> https://artalk.js.org
|
||||
`
|
||||
|
||||
type ArtalkCmd struct {
|
||||
*core.App
|
||||
|
||||
cfgFile string
|
||||
workDir string
|
||||
|
||||
RootCmd *cobra.Command
|
||||
}
|
||||
|
||||
const BootModeKey = "BootMode"
|
||||
|
||||
const (
|
||||
MODE_FULL_BOOT = "FULL_BOOT"
|
||||
MODE_MINI_BOOT = "MINI_BOOT"
|
||||
)
|
||||
|
||||
func New() *ArtalkCmd {
|
||||
cmd := &ArtalkCmd{
|
||||
RootCmd: &cobra.Command{
|
||||
Use: "artalk",
|
||||
Short: "Artalk: Your self-hosted comment system",
|
||||
Long: Banner,
|
||||
Version: Version,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(Banner)
|
||||
fmt.Print("-------------------------------\n\n")
|
||||
fmt.Println("NOTE: add `-h` flag to show help about any command.")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cmd.RootCmd.SetVersionTemplate("Artalk ({{printf \"%s\" .Version}})\n")
|
||||
|
||||
// Parse base flags
|
||||
cmd.eagerParseFlags()
|
||||
|
||||
// Change work directory
|
||||
if cmd.workDir != "" {
|
||||
if err := os.Chdir(cmd.workDir); err != nil {
|
||||
log.Fatal("Working directory change error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Parses the global app flags before calling atk.RootCmd.Execute().
|
||||
// so we can have all flags ready for use on initialization.
|
||||
func (atk *ArtalkCmd) eagerParseFlags() error {
|
||||
atk.RootCmd.PersistentFlags().StringVarP(&atk.cfgFile, "config", "c", "", "config file path (defaults are './artalk.yml')")
|
||||
atk.RootCmd.PersistentFlags().StringVarP(&atk.workDir, "workdir", "w", "", "program working directory (defaults are './')")
|
||||
|
||||
return atk.RootCmd.ParseFlags(os.Args[1:])
|
||||
}
|
||||
|
||||
func (atk *ArtalkCmd) addCommand(cmd *cobra.Command) {
|
||||
originalPreRunFunc := cmd.PreRun
|
||||
|
||||
cmd.PreRun = func(cmd *cobra.Command, args []string) {
|
||||
// ================
|
||||
// APP Bootstrap
|
||||
// ================
|
||||
if cmd.Annotations[BootModeKey] != string(MODE_MINI_BOOT) {
|
||||
// Load config
|
||||
config, err := getConfig(atk.cfgFile)
|
||||
if err != nil {
|
||||
log.Fatal("Config fail: ", err)
|
||||
}
|
||||
|
||||
// Create new instance
|
||||
atk.App = core.NewApp(config)
|
||||
|
||||
// Bootstrap APP
|
||||
if err := atk.App.Bootstrap(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
if originalPreRunFunc != nil {
|
||||
originalPreRunFunc(cmd, args) // extends original pre-run logic
|
||||
}
|
||||
}
|
||||
|
||||
atk.RootCmd.AddCommand(cmd)
|
||||
}
|
||||
|
||||
func (atk *ArtalkCmd) mountCommands() {
|
||||
atk.addCommand(NewServeCommand(atk))
|
||||
atk.addCommand(NewAdminCommand(atk))
|
||||
atk.addCommand(NewExportCommand(atk))
|
||||
atk.addCommand(NewImportCommand(atk))
|
||||
atk.addCommand(NewConfigCommand())
|
||||
atk.addCommand(NewGenCommand())
|
||||
atk.addCommand(NewUpgradeCommand())
|
||||
atk.addCommand(NewVersionCommand())
|
||||
}
|
||||
|
||||
func (atk *ArtalkCmd) Launch() error {
|
||||
// ===================
|
||||
// 1. Prepare Commands
|
||||
// ===================
|
||||
atk.mountCommands()
|
||||
|
||||
done := make(chan bool, 1) // shutdown signal
|
||||
|
||||
// listen for interrupt signal to gracefully shutdown the application
|
||||
go func() {
|
||||
sigch := make(chan os.Signal, 1)
|
||||
signal.Notify(sigch, os.Interrupt, syscall.SIGTERM)
|
||||
<-sigch
|
||||
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// ===================
|
||||
// 2. Command Execute
|
||||
// ===================
|
||||
go func() {
|
||||
if err := atk.RootCmd.Execute(); err != nil {
|
||||
color.Red(err.Error())
|
||||
}
|
||||
|
||||
done <- true
|
||||
}()
|
||||
|
||||
<-done
|
||||
|
||||
// ===================
|
||||
// 3. App Cleanups
|
||||
// ===================
|
||||
if atk.App != nil {
|
||||
return atk.App.OnTerminate().Trigger(&core.TerminateEvent{
|
||||
App: atk.App,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create new config instance by specific config filename
|
||||
func getConfig(cfgFile string) (*config.Config, error) {
|
||||
// Retrieve config file by default names when specific filename is empty
|
||||
if cfgFile == "" {
|
||||
cfgFile = config.RetrieveConfigFile()
|
||||
}
|
||||
|
||||
// Generate new config file when retrieve failed
|
||||
if cfgFile == "" {
|
||||
cfgFile = config.CONF_DEFAULT_FILENAMES[0]
|
||||
core.Gen("config", cfgFile, false)
|
||||
}
|
||||
|
||||
// Create new config instance and return
|
||||
return config.NewFromFile(cfgFile)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Shortcut Functions
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
func flag(cmd *cobra.Command, name string, defaultVal interface{}, usage string) {
|
||||
f := cmd.PersistentFlags()
|
||||
switch y := defaultVal.(type) {
|
||||
case bool:
|
||||
f.Bool(name, y, usage)
|
||||
case int:
|
||||
f.Int(name, y, usage)
|
||||
case string:
|
||||
f.String(name, y, usage)
|
||||
}
|
||||
}
|
||||
|
||||
func flagP(cmd *cobra.Command, name, shorthand string, defaultVal interface{}, usage string) {
|
||||
f := cmd.PersistentFlags()
|
||||
switch y := defaultVal.(type) {
|
||||
case bool:
|
||||
f.BoolP(name, shorthand, y, usage)
|
||||
case int:
|
||||
f.IntP(name, shorthand, y, usage)
|
||||
case string:
|
||||
f.StringP(name, shorthand, y, usage)
|
||||
}
|
||||
}
|
||||
|
||||
func flagV(cmd *cobra.Command, name string, defaultVal interface{}, usage string) {
|
||||
flag(cmd, name, defaultVal, usage)
|
||||
}
|
||||
|
||||
func flagPV(cmd *cobra.Command, name, shorthand string, defaultVal interface{}, usage string) {
|
||||
flagP(cmd, name, shorthand, defaultVal, usage)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewConfigCommand() *cobra.Command {
|
||||
configCmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Output Config Information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Get config filename from cmd flags
|
||||
filename, _ := cmd.Flags().GetString("config")
|
||||
|
||||
// Get new config instance
|
||||
config, err := getConfig(filename)
|
||||
if err != nil {
|
||||
log.Fatal("Config fail: ", err)
|
||||
}
|
||||
|
||||
// Output JSON of config
|
||||
buf, _ := json.MarshalIndent(config, "", " ")
|
||||
fmt.Println(string(buf))
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
BootModeKey: MODE_MINI_BOOT,
|
||||
},
|
||||
}
|
||||
|
||||
return configCmd
|
||||
}
|
||||
+47
-50
@@ -8,68 +8,65 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/artransfer"
|
||||
"github.com/ArtalkJS/Artalk/internal/core"
|
||||
"github.com/ArtalkJS/Artalk/internal/i18n"
|
||||
"github.com/ArtalkJS/Artalk/internal/log"
|
||||
"github.com/ArtalkJS/Artalk/internal/utils"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var exportCmd = &cobra.Command{
|
||||
Use: "export",
|
||||
Aliases: []string{},
|
||||
Short: "Artransfer - Export",
|
||||
Long: "\n# Artransfer - Export\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
core.LoadCore(cfgFile, workDir)
|
||||
|
||||
jsonStr, err := artransfer.ExportArtransString()
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
|
||||
if len(args) < 1 || args[0] == "" {
|
||||
// write to stdout
|
||||
fmt.Println(jsonStr)
|
||||
} else {
|
||||
filename := args[0]
|
||||
|
||||
// make sure is abs path
|
||||
filename, err := filepath.Abs(filename)
|
||||
func NewExportCommand(app *ArtalkCmd) *cobra.Command {
|
||||
exportCmd := &cobra.Command{
|
||||
Use: "export",
|
||||
Aliases: []string{},
|
||||
Short: "Artransfer export",
|
||||
Long: "\n# Artransfer - Export\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
jsonStr, err := artransfer.RunExportArtrans(app.Dao(), &artransfer.ExportParams{})
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// check dir
|
||||
stat, err := os.Stat(filename)
|
||||
if err == nil {
|
||||
if stat.IsDir() {
|
||||
filename = path.Join(filename, "backup-"+time.Now().Format("20060102-150405")+".artrans")
|
||||
if len(args) < 1 || args[0] == "" {
|
||||
// write to stdout
|
||||
fmt.Println(jsonStr)
|
||||
} else {
|
||||
filename := args[0]
|
||||
|
||||
// make sure is abs path
|
||||
filename, err := filepath.Abs(filename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// mkdir -p
|
||||
if err := utils.EnsureDir(filepath.Dir(filename)); err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
// check dir
|
||||
stat, err := os.Stat(filename)
|
||||
if err == nil {
|
||||
if stat.IsDir() {
|
||||
filename = path.Join(filename, "backup-"+time.Now().Format("20060102-150405")+".artrans")
|
||||
}
|
||||
}
|
||||
|
||||
// touch
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
// mkdir -p
|
||||
if err := utils.EnsureDir(filepath.Dir(filename)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// >
|
||||
_, err2 := f.WriteString(jsonStr)
|
||||
if err2 != nil {
|
||||
logrus.Fatal(err2)
|
||||
}
|
||||
// touch
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
logrus.Info(i18n.T("Export complete") + ": " + filename)
|
||||
}
|
||||
},
|
||||
}
|
||||
// >
|
||||
_, err2 := f.WriteString(jsonStr)
|
||||
if err2 != nil {
|
||||
log.Fatal(err2)
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(exportCmd)
|
||||
log.Info(i18n.T("Export complete") + ": " + filename)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return exportCmd
|
||||
}
|
||||
+22
-29
@@ -1,41 +1,34 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/core"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var genCmd = &cobra.Command{
|
||||
Use: "gen <TYPE> <DEST>",
|
||||
Short: "A collection of several useful generators",
|
||||
Long: "Generate some content\ne.g. `artalk gen config ./artalk.yml`",
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// change working directory
|
||||
if workDir != "" {
|
||||
if err := os.Chdir(workDir); err != nil {
|
||||
logrus.Fatal("change working directory error ", err)
|
||||
func NewGenCommand() *cobra.Command {
|
||||
genCmd := &cobra.Command{
|
||||
Use: "gen <TYPE> <DEST>",
|
||||
Short: "A collection of several useful generators",
|
||||
Long: "Generate some content\ne.g. `artalk gen config ./artalk.yml`",
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
specificPath string
|
||||
isForce bool
|
||||
)
|
||||
if len(args) > 1 {
|
||||
specificPath = args[1]
|
||||
}
|
||||
}
|
||||
isForce, _ = cmd.Flags().GetBool("force")
|
||||
|
||||
var (
|
||||
specificPath string
|
||||
isForce bool
|
||||
)
|
||||
if len(args) > 1 {
|
||||
specificPath = args[1]
|
||||
}
|
||||
isForce, _ = cmd.Flags().GetBool("force")
|
||||
|
||||
core.Gen(args[0], specificPath, isForce)
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(genCmd)
|
||||
core.Gen(args[0], specificPath, isForce)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
BootModeKey: MODE_MINI_BOOT,
|
||||
},
|
||||
}
|
||||
|
||||
flagPV(genCmd, "force", "f", false, "Force overwrite an existing file")
|
||||
|
||||
return genCmd
|
||||
}
|
||||
+24
-25
@@ -5,38 +5,37 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/artransfer"
|
||||
"github.com/ArtalkJS/Artalk/internal/core"
|
||||
"github.com/ArtalkJS/Artalk/internal/i18n"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/ArtalkJS/Artalk/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var importCmd = &cobra.Command{
|
||||
Use: "import <FILENAME>",
|
||||
Aliases: []string{},
|
||||
Short: "Artransfer - Import",
|
||||
Long: "\n# Artransfer - Import\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
core.LoadCore(cfgFile, workDir) // load core
|
||||
func NewImportCommand(app *ArtalkCmd) *cobra.Command {
|
||||
importCmd := &cobra.Command{
|
||||
Use: "import <FILENAME>",
|
||||
Aliases: []string{},
|
||||
Short: "Artransfer import",
|
||||
Long: "\n# Artransfer - Import\n\n See the documentation to learn more: https://artalk.js.org/guide/transfer.html",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
parcelFile := args[0]
|
||||
if _, err := os.Stat(parcelFile); errors.Is(err, os.ErrNotExist) {
|
||||
log.Fatal(i18n.T("{{name}} not found", map[string]interface{}{"name": i18n.T("File")}))
|
||||
}
|
||||
|
||||
parcelFile := args[0]
|
||||
if _, err := os.Stat(parcelFile); errors.Is(err, os.ErrNotExist) {
|
||||
logrus.Fatal(i18n.T("{{name}} not found", map[string]interface{}{"name": i18n.T("File")}))
|
||||
}
|
||||
payload := args[1:]
|
||||
payload = append(payload, "json_file:"+parcelFile)
|
||||
|
||||
payload := args[1:]
|
||||
payload = append(payload, "json_file:"+parcelFile)
|
||||
// import Artrans
|
||||
params := artransfer.ArrToImportParams(payload)
|
||||
params.Assumeyes, _ = cmd.Flags().GetBool("assumeyes")
|
||||
artransfer.RunImportArtrans(app.Dao(), params)
|
||||
|
||||
// import Artrans
|
||||
artransfer.RunImportArtrans(payload)
|
||||
|
||||
logrus.Info(i18n.T("Import complete"))
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(importCmd)
|
||||
log.Info(i18n.T("Import complete"))
|
||||
},
|
||||
}
|
||||
|
||||
flagPV(importCmd, "assumeyes", "y", false, "Automatically answer yes for all questions.")
|
||||
|
||||
return importCmd
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/config"
|
||||
"github.com/ArtalkJS/Artalk/internal/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Version = config.Version + `/` + config.CommitHash
|
||||
|
||||
var Banner = `
|
||||
________ ________ _________ ________ ___ ___ __
|
||||
|\ __ \|\ __ \|\___ ___\\ __ \|\ \ |\ \|\ \
|
||||
\ \ \|\ \ \ \|\ \|___ \ \_\ \ \|\ \ \ \ \ \ \/ /|_
|
||||
\ \ __ \ \ _ _\ \ \ \ \ \ __ \ \ \ \ \ ___ \
|
||||
\ \ \ \ \ \ \\ \| \ \ \ \ \ \ \ \ \ \____\ \ \\ \ \
|
||||
\ \__\ \__\ \__\\ _\ \ \__\ \ \__\ \__\ \_______\ \__\\ \__\
|
||||
\|__|\|__|\|__|\|__| \|__| \|__|\|__|\|_______|\|__| \|__|
|
||||
|
||||
Artalk (` + Version + `)
|
||||
|
||||
-> A Selfhosted Comment System.
|
||||
-> https://artalk.js.org
|
||||
`
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
workDir string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "artalk",
|
||||
Short: "Artalk: A Fast, Slight & Delightful Comment System",
|
||||
Long: Banner,
|
||||
Version: Version,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(Banner)
|
||||
fmt.Print("-------------------------------\n\n")
|
||||
fmt.Println("NOTE: add `-h` flag to show help about any command.")
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
cobra.CheckErr(rootCmd.Execute())
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.SetVersionTemplate("Artalk ({{printf \"%s\" .Version}})\n")
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file path (defaults are './artalk.yml')")
|
||||
rootCmd.PersistentFlags().StringVarP(&workDir, "workdir", "w", "", "program working directory (defaults are './')")
|
||||
|
||||
// Version Command
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Output Version Information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Artalk (" + Version + ")")
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
// Config Command
|
||||
configCmd := &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Output Config Information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
core.LoadConfOnly(cfgFile, workDir)
|
||||
buf, _ := json.MarshalIndent(config.Instance, "", " ")
|
||||
fmt.Println(string(buf))
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(configCmd)
|
||||
|
||||
}
|
||||
+21
-72
@@ -2,85 +2,34 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/config"
|
||||
"github.com/ArtalkJS/Artalk/internal/core"
|
||||
"github.com/ArtalkJS/Artalk/internal/log"
|
||||
"github.com/ArtalkJS/Artalk/server"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Aliases: []string{"serve"},
|
||||
Short: "Start the server",
|
||||
Long: Banner,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
core.LoadCore(cfgFile, workDir)
|
||||
func NewServeCommand(app *ArtalkCmd) *cobra.Command {
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Aliases: []string{"serve"},
|
||||
Short: "Start the server",
|
||||
Long: Banner,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(Banner)
|
||||
fmt.Print("-------------------------------\n\n")
|
||||
|
||||
fmt.Println(Banner)
|
||||
fmt.Print("-------------------------------\n\n")
|
||||
|
||||
// create fiber app
|
||||
// @see https://docs.gofiber.io/api/fiber#config
|
||||
app := fiber.New(fiber.Config{
|
||||
// @see https://github.com/gofiber/fiber/issues/426
|
||||
// @see https://github.com/gofiber/fiber/issues/185
|
||||
Immutable: true,
|
||||
|
||||
// TODO add config to set ProxyHeader
|
||||
ProxyHeader: "X-Forwarded-For",
|
||||
EnableIPValidation: true,
|
||||
})
|
||||
|
||||
// logger
|
||||
app.Use(logger.New(logger.Config{
|
||||
Format: "[${status}] ${method} ${path} ${latency} ${ip} ${reqHeader:X-Request-ID} ${referer} ${ua}\n",
|
||||
Output: io.Discard,
|
||||
Done: func(c *fiber.Ctx, logString []byte) {
|
||||
statusOK := c.Response().StatusCode() >= 200 && c.Response().StatusCode() <= 302
|
||||
if !statusOK {
|
||||
logrus.StandardLogger().WriterLevel(logrus.ErrorLevel).Write(logString)
|
||||
} else {
|
||||
logrus.StandardLogger().WriterLevel(logrus.DebugLevel).Write(logString)
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
// init router
|
||||
server.Init(app)
|
||||
|
||||
// graceful shutdown signal listen
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt)
|
||||
go func() {
|
||||
<-c
|
||||
fmt.Println("Gracefully shutting down...")
|
||||
_ = app.Shutdown()
|
||||
}()
|
||||
|
||||
// socket listen
|
||||
listenAddr := fmt.Sprintf("%s:%d", config.Instance.Host, config.Instance.Port)
|
||||
if config.Instance.SSL.Enabled {
|
||||
app.ListenTLS(listenAddr, config.Instance.SSL.CertPath, config.Instance.SSL.KeyPath)
|
||||
} else {
|
||||
app.Listen(listenAddr)
|
||||
}
|
||||
|
||||
// graceful shutdown tasks
|
||||
fmt.Println("Running cleanup tasks...")
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serverCmd)
|
||||
// init fiber app
|
||||
_, err := server.Serve(app.App)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
flagPV(serverCmd, "host", "", "0.0.0.0", "Listening IP")
|
||||
flagPV(serverCmd, "port", "", 23366, "Listening port")
|
||||
|
||||
return serverCmd
|
||||
}
|
||||
+44
-44
@@ -6,63 +6,63 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/config"
|
||||
"github.com/ArtalkJS/Artalk/internal/core"
|
||||
"github.com/ArtalkJS/Artalk/internal/i18n"
|
||||
"github.com/ArtalkJS/Artalk/internal/log"
|
||||
"github.com/blang/semver"
|
||||
"github.com/rhysd/go-github-selfupdate/selfupdate"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var upgradeCmd = &cobra.Command{
|
||||
Use: "upgrade",
|
||||
Aliases: []string{"update"},
|
||||
Short: "Upgrade to the latest version",
|
||||
Long: "Upgrade Artalk to the latest version, \n update source is GitHub Releases, \n update need to restart Artalk to take effect.",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// loadCore()
|
||||
core.LoadConfOnly(cfgFile, workDir)
|
||||
func NewUpgradeCommand() *cobra.Command {
|
||||
upgradeCmd := &cobra.Command{
|
||||
Use: "upgrade",
|
||||
Aliases: []string{"update"},
|
||||
Short: "Upgrade to the latest version",
|
||||
Long: "Upgrade Artalk to the latest version, \n update source is GitHub Releases, \n update need to restart Artalk to take effect.",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
log.Info(i18n.T("Checking for updates") + "...")
|
||||
|
||||
logrus.Info(i18n.T("Checking for updates") + "...")
|
||||
|
||||
latest, found, err := selfupdate.DetectLatest("ArtalkJS/Artalk")
|
||||
if err != nil {
|
||||
logrus.Fatal("Error occurred while detecting version: ", err)
|
||||
}
|
||||
|
||||
ignoreVersionCheck, _ := cmd.Flags().GetBool("force")
|
||||
if !ignoreVersionCheck {
|
||||
v := semver.MustParse(strings.TrimPrefix(config.Version, "v"))
|
||||
if !found || latest.Version.LTE(v) {
|
||||
logrus.Println(i18n.T("Current version is the latest") + " (v" + v.String() + ")")
|
||||
return
|
||||
latest, found, err := selfupdate.DetectLatest("ArtalkJS/Artalk")
|
||||
if err != nil {
|
||||
log.Fatal("Error occurred while detecting version: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Info(i18n.T("New version available") + ": v" + latest.Version.String())
|
||||
logrus.Info(i18n.T("Downloading") + "...")
|
||||
ignoreVersionCheck, _ := cmd.Flags().GetBool("force")
|
||||
if !ignoreVersionCheck {
|
||||
v := semver.MustParse(strings.TrimPrefix(config.Version, "v"))
|
||||
if !found || latest.Version.LTE(v) {
|
||||
log.Println(i18n.T("Current version is the latest") + " (v" + v.String() + ")")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
logrus.Fatal("Could not locate executable path ", err)
|
||||
}
|
||||
log.Info(i18n.T("New version available") + ": v" + latest.Version.String())
|
||||
log.Info(i18n.T("Downloading") + "...")
|
||||
|
||||
if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil {
|
||||
logrus.Fatal(i18n.T("Update failed")+" ", err)
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
log.Fatal("Could not locate executable path ", err)
|
||||
}
|
||||
|
||||
logrus.Println(i18n.T("Update complete"))
|
||||
fmt.Println("\n-------------------------------\n v" +
|
||||
latest.Version.String() +
|
||||
" Release Note\n" +
|
||||
"-------------------------------\n\n" +
|
||||
latest.ReleaseNotes)
|
||||
},
|
||||
}
|
||||
if err := selfupdate.UpdateTo(latest.AssetURL, exe); err != nil {
|
||||
log.Fatal(i18n.T("Update failed")+" ", err)
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(upgradeCmd)
|
||||
log.Println(i18n.T("Update complete"))
|
||||
fmt.Printf("\n"+
|
||||
"-------------------------------\n"+
|
||||
" v%s Release Note\n"+
|
||||
"-------------------------------\n\n"+
|
||||
"%s\n",
|
||||
latest.Version.String(), latest.ReleaseNotes)
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
BootModeKey: MODE_MINI_BOOT,
|
||||
},
|
||||
}
|
||||
|
||||
flagPV(upgradeCmd, "force", "f", false, "Force upgrade ignore version comparison.")
|
||||
|
||||
return upgradeCmd
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewVersionCommand() *cobra.Command {
|
||||
versionCmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Output version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("Artalk (" + Version + ")")
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
BootModeKey: MODE_MINI_BOOT,
|
||||
},
|
||||
}
|
||||
|
||||
return versionCmd
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,2 @@
|
||||
shamefully-hoist=true
|
||||
auto-install-peers=true
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"latest": "2.5.2"
|
||||
"latest": "2.6.2"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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/)
|
||||
|
||||
更多新版读物正在撰写当中(
|
||||
|
||||
|
||||
@@ -350,12 +350,24 @@ gravatar: {
|
||||
>
|
||||
> loli:https://gravatar.loli.net/avatar/
|
||||
|
||||
### gravatar.default
|
||||
### gravatar.params
|
||||
|
||||
**默认头像**(URL or [Gravatar Type](http://cn.gravatar.org/site/implement/images/#default-image))
|
||||
**Gravatar API 参数**
|
||||
|
||||
- 类型:`String`
|
||||
- 默认值:`"mp"`
|
||||
- 默认值:`"d=mp&s=240"`
|
||||
|
||||
例如,你可以通过该配置项设置默认头像 (`d=mp`) 和头像尺寸 (`s=240`)。
|
||||
|
||||
参考:[Gravatar API 文档](http://cn.gravatar.org/site/implement/images/)
|
||||
|
||||
该配置项格式为 HTTP Query。
|
||||
|
||||
::: warning 更新注意
|
||||
|
||||
v2.5.5 已废弃 `gravatar.default` 配置项,请使用 `gravatar.params` 替代。
|
||||
|
||||
:::
|
||||
|
||||
### avatarURLBuilder
|
||||
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ Artalk 社区是包容开放的社区,我们欢迎不同水平的人员帮助
|
||||
|
||||
“More action, less talk”,Artalk 社区不欢迎无意义的争论,我们希望社区成员和谐共处、为社区发展出谋划策。在提出问题前,你应当读过《[提问的智慧](https://lug.ustc.edu.cn/wiki/doc/smart-questions/)》,这可能决定了你最终是否能得到有用的回答。在表达观点前,你应当具备基本的礼仪,比如保持平和的态度、使用得体的语言,切忌恶语相向、冷嘲热讽、不尊重他人信仰和立场等。
|
||||
|
||||
我们作为开源精神的推崇者以及实践者,希望我们所创造的自由软件,都应该被自由的使用,自由的研究,自由的更改和自由的分享。本项目前后端代码使用第 3 版 GNU 宽通用公共许可证([GNU Lesser General Public License V3](https://www.gnu.org/licenses/lgpl-3.0.html))开源,文档请遵循知识共享许可协议 ([Creative Commons License](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh))。
|
||||
我们作为开源精神的推崇者以及实践者,希望我们所创造的自由软件,都应该被自由的使用,自由的研究,自由的更改和自由的分享。本项目前后端代码使用 [MIT](https://github.com/ArtalkJS/Artalk/blob/master/LICENSE) 协议开源,文档请遵循知识共享许可协议 ([Creative Commons License](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh))。
|
||||
::: tip 立即为社区贡献力量?
|
||||
|
||||
- 浏览开发者资料([开发文档](../develop/index.md) / [CONTRIBUTING.md](https://github.com/ArtalkJS/Artalk/blob/master/CONTRIBUTING.md))
|
||||
|
||||
+4
-3
@@ -2,6 +2,7 @@
|
||||
"name": "docs",
|
||||
"version": "2.0.0",
|
||||
"description": "Artalk documentation",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ArtalkJS/Artalk",
|
||||
@@ -16,9 +17,9 @@
|
||||
"swagger:serve": "npx @redocly/cli preview-docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redocly/cli": "1.0.0-beta.124",
|
||||
"@redocly/cli": "1.0.2",
|
||||
"markdown-it-for-inline": "^0.1.1",
|
||||
"sass": "^1.59.3",
|
||||
"vitepress": "1.0.0-alpha.60"
|
||||
"sass": "^1.66.1",
|
||||
"vitepress": "1.0.0-rc.4"
|
||||
}
|
||||
}
|
||||
Generated
+1354
-680
File diff suppressed because it is too large.
Load diff
+112
-4
@@ -1827,9 +1827,16 @@ const docTemplate = `{
|
||||
],
|
||||
"summary": "Notify Mark Read",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "the comment id of the notify you want to mark as read",
|
||||
"name": "comment_id",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the notify key you want to mark as read",
|
||||
"description": "the key of the notify",
|
||||
"name": "notify_key",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
@@ -2120,9 +2127,6 @@ const docTemplate = `{
|
||||
"commit_hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"fe_min_version": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2373,14 +2377,43 @@ const docTemplate = `{
|
||||
"always": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"captcha_type": {
|
||||
"$ref": "#/definitions/config.CaptchaType"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"geetest": {
|
||||
"$ref": "#/definitions/config.GeetestConf"
|
||||
},
|
||||
"hcaptcha": {
|
||||
"$ref": "#/definitions/config.HCaptchaConf"
|
||||
},
|
||||
"recaptcha": {
|
||||
"$ref": "#/definitions/config.ReCaptchaConf"
|
||||
},
|
||||
"turnstile": {
|
||||
"$ref": "#/definitions/config.TurnstileConf"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.CaptchaType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"image",
|
||||
"turnstile",
|
||||
"recaptcha",
|
||||
"hcaptcha",
|
||||
"geetest"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TypeImage",
|
||||
"TypeTurnstile",
|
||||
"TypeReCaptcha",
|
||||
"TypeHCaptcha",
|
||||
"TypeGeetest"
|
||||
]
|
||||
},
|
||||
"config.Config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2463,6 +2496,14 @@ const docTemplate = `{
|
||||
}
|
||||
]
|
||||
},
|
||||
"ip_region": {
|
||||
"description": "IP 归属地展示",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.IPRegionConf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"locale": {
|
||||
"description": "语言",
|
||||
"type": "string"
|
||||
@@ -2598,6 +2639,14 @@ const docTemplate = `{
|
||||
"description": "邮件模板",
|
||||
"type": "string"
|
||||
},
|
||||
"queue": {
|
||||
"description": "邮件发送队列配置",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.EmailQueueConf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"send_addr": {
|
||||
"description": "发件人地址",
|
||||
"type": "string"
|
||||
@@ -2624,6 +2673,15 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.EmailQueueConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"buffer_size": {
|
||||
"description": "Channel buffer size (default is zero that not create buffer)",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.EmailSenderType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -2645,9 +2703,34 @@ const docTemplate = `{
|
||||
},
|
||||
"captcha_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.HCaptchaConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.IPRegionConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"db_path": {
|
||||
"description": "数据文件路径",
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"description": "启用 IP 归属地展示",
|
||||
"type": "boolean"
|
||||
},
|
||||
"precision": {
|
||||
"description": "显示精度",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2843,6 +2926,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.ReCaptchaConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.RedisConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2913,6 +3007,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.TurnstileConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.UpgitConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2954,6 +3059,9 @@ const docTemplate = `{
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"ip_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_allow_reply": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
+112
-4
@@ -1820,9 +1820,16 @@
|
||||
],
|
||||
"summary": "Notify Mark Read",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "the comment id of the notify you want to mark as read",
|
||||
"name": "comment_id",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the notify key you want to mark as read",
|
||||
"description": "the key of the notify",
|
||||
"name": "notify_key",
|
||||
"in": "formData",
|
||||
"required": true
|
||||
@@ -2113,9 +2120,6 @@
|
||||
"commit_hash": {
|
||||
"type": "string"
|
||||
},
|
||||
"fe_min_version": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -2366,14 +2370,43 @@
|
||||
"always": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"captcha_type": {
|
||||
"$ref": "#/definitions/config.CaptchaType"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"geetest": {
|
||||
"$ref": "#/definitions/config.GeetestConf"
|
||||
},
|
||||
"hcaptcha": {
|
||||
"$ref": "#/definitions/config.HCaptchaConf"
|
||||
},
|
||||
"recaptcha": {
|
||||
"$ref": "#/definitions/config.ReCaptchaConf"
|
||||
},
|
||||
"turnstile": {
|
||||
"$ref": "#/definitions/config.TurnstileConf"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.CaptchaType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"image",
|
||||
"turnstile",
|
||||
"recaptcha",
|
||||
"hcaptcha",
|
||||
"geetest"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TypeImage",
|
||||
"TypeTurnstile",
|
||||
"TypeReCaptcha",
|
||||
"TypeHCaptcha",
|
||||
"TypeGeetest"
|
||||
]
|
||||
},
|
||||
"config.Config": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2456,6 +2489,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ip_region": {
|
||||
"description": "IP 归属地展示",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.IPRegionConf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"locale": {
|
||||
"description": "语言",
|
||||
"type": "string"
|
||||
@@ -2591,6 +2632,14 @@
|
||||
"description": "邮件模板",
|
||||
"type": "string"
|
||||
},
|
||||
"queue": {
|
||||
"description": "邮件发送队列配置",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/config.EmailQueueConf"
|
||||
}
|
||||
]
|
||||
},
|
||||
"send_addr": {
|
||||
"description": "发件人地址",
|
||||
"type": "string"
|
||||
@@ -2617,6 +2666,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.EmailQueueConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"buffer_size": {
|
||||
"description": "Channel buffer size (default is zero that not create buffer)",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.EmailSenderType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -2638,9 +2696,34 @@
|
||||
},
|
||||
"captcha_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.HCaptchaConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.IPRegionConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"db_path": {
|
||||
"description": "数据文件路径",
|
||||
"type": "string"
|
||||
},
|
||||
"enabled": {
|
||||
"description": "启用 IP 归属地展示",
|
||||
"type": "boolean"
|
||||
},
|
||||
"precision": {
|
||||
"description": "显示精度",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2836,6 +2919,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.ReCaptchaConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.RedisConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2906,6 +3000,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.TurnstileConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"site_key": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"config.UpgitConf": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2947,6 +3052,9 @@
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"ip_region": {
|
||||
"type": "string"
|
||||
},
|
||||
"is_allow_reply": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -6,8 +6,6 @@ definitions:
|
||||
type: string
|
||||
commit_hash:
|
||||
type: string
|
||||
fe_min_version:
|
||||
type: string
|
||||
version:
|
||||
type: string
|
||||
type: object
|
||||
@@ -168,11 +166,33 @@ definitions:
|
||||
type: integer
|
||||
always:
|
||||
type: boolean
|
||||
captcha_type:
|
||||
$ref: '#/definitions/config.CaptchaType'
|
||||
enabled:
|
||||
type: boolean
|
||||
geetest:
|
||||
$ref: '#/definitions/config.GeetestConf'
|
||||
hcaptcha:
|
||||
$ref: '#/definitions/config.HCaptchaConf'
|
||||
recaptcha:
|
||||
$ref: '#/definitions/config.ReCaptchaConf'
|
||||
turnstile:
|
||||
$ref: '#/definitions/config.TurnstileConf'
|
||||
type: object
|
||||
config.CaptchaType:
|
||||
enum:
|
||||
- image
|
||||
- turnstile
|
||||
- recaptcha
|
||||
- hcaptcha
|
||||
- geetest
|
||||
type: string
|
||||
x-enum-varnames:
|
||||
- TypeImage
|
||||
- TypeTurnstile
|
||||
- TypeReCaptcha
|
||||
- TypeHCaptcha
|
||||
- TypeGeetest
|
||||
config.Config:
|
||||
properties:
|
||||
admin_notify:
|
||||
@@ -220,6 +240,10 @@ definitions:
|
||||
allOf:
|
||||
- $ref: '#/definitions/config.ImgUploadConf'
|
||||
description: 图片上传
|
||||
ip_region:
|
||||
allOf:
|
||||
- $ref: '#/definitions/config.IPRegionConf'
|
||||
description: IP 归属地展示
|
||||
locale:
|
||||
description: 语言
|
||||
type: string
|
||||
@@ -309,6 +333,10 @@ definitions:
|
||||
mail_tpl:
|
||||
description: 邮件模板
|
||||
type: string
|
||||
queue:
|
||||
allOf:
|
||||
- $ref: '#/definitions/config.EmailQueueConf'
|
||||
description: 邮件发送队列配置
|
||||
send_addr:
|
||||
description: 发件人地址
|
||||
type: string
|
||||
@@ -324,6 +352,12 @@ definitions:
|
||||
- $ref: '#/definitions/config.SMTPConf'
|
||||
description: SMTP 配置
|
||||
type: object
|
||||
config.EmailQueueConf:
|
||||
properties:
|
||||
buffer_size:
|
||||
description: Channel buffer size (default is zero that not create buffer)
|
||||
type: integer
|
||||
type: object
|
||||
config.EmailSenderType:
|
||||
enum:
|
||||
- smtp
|
||||
@@ -340,8 +374,25 @@ definitions:
|
||||
type: string
|
||||
captcha_key:
|
||||
type: string
|
||||
type: object
|
||||
config.HCaptchaConf:
|
||||
properties:
|
||||
secret_key:
|
||||
type: string
|
||||
site_key:
|
||||
type: string
|
||||
type: object
|
||||
config.IPRegionConf:
|
||||
properties:
|
||||
db_path:
|
||||
description: 数据文件路径
|
||||
type: string
|
||||
enabled:
|
||||
description: 启用 IP 归属地展示
|
||||
type: boolean
|
||||
precision:
|
||||
description: 显示精度
|
||||
type: string
|
||||
type: object
|
||||
config.ImgUploadConf:
|
||||
properties:
|
||||
@@ -468,6 +519,13 @@ definitions:
|
||||
url:
|
||||
type: string
|
||||
type: object
|
||||
config.ReCaptchaConf:
|
||||
properties:
|
||||
secret_key:
|
||||
type: string
|
||||
site_key:
|
||||
type: string
|
||||
type: object
|
||||
config.RedisConf:
|
||||
properties:
|
||||
db:
|
||||
@@ -514,6 +572,13 @@ definitions:
|
||||
secret_key:
|
||||
type: string
|
||||
type: object
|
||||
config.TurnstileConf:
|
||||
properties:
|
||||
secret_key:
|
||||
type: string
|
||||
site_key:
|
||||
type: string
|
||||
type: object
|
||||
config.UpgitConf:
|
||||
properties:
|
||||
del_local:
|
||||
@@ -542,6 +607,8 @@ definitions:
|
||||
type: string
|
||||
id:
|
||||
type: integer
|
||||
ip_region:
|
||||
type: string
|
||||
is_allow_reply:
|
||||
type: boolean
|
||||
is_collapsed:
|
||||
@@ -1905,7 +1972,12 @@ paths:
|
||||
post:
|
||||
description: Mark specific notification as read for user
|
||||
parameters:
|
||||
- description: the notify key you want to mark as read
|
||||
- description: the comment id of the notify you want to mark as read
|
||||
in: formData
|
||||
name: comment_id
|
||||
required: true
|
||||
type: integer
|
||||
- description: the key of the notify
|
||||
in: formData
|
||||
name: notify_key
|
||||
required: true
|
||||
|
||||
@@ -1,56 +1,59 @@
|
||||
module github.com/ArtalkJS/Artalk
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/allegro/bigcache/v3 v3.1.0
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/blang/semver v3.5.1+incompatible
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746
|
||||
github.com/cheggaaa/pb/v3 v3.1.2
|
||||
github.com/eko/gocache/lib/v4 v4.1.3
|
||||
github.com/eko/gocache/store/bigcache/v4 v4.1.2
|
||||
github.com/eko/gocache/store/memcache/v4 v4.1.2
|
||||
github.com/eko/gocache/store/redis/v4 v4.1.2
|
||||
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.8.1
|
||||
github.com/gofiber/fiber/v2 v2.42.0
|
||||
github.com/gofiber/swagger v0.1.9
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285
|
||||
github.com/cheggaaa/pb/v3 v3.1.4
|
||||
github.com/eko/gocache/lib/v4 v4.1.4
|
||||
github.com/eko/gocache/store/bigcache/v4 v4.2.0
|
||||
github.com/eko/gocache/store/memcache/v4 v4.2.0
|
||||
github.com/eko/gocache/store/redis/v4 v4.2.0
|
||||
github.com/fatih/color v1.15.0
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.9.0
|
||||
github.com/gofiber/fiber/v2 v2.48.0
|
||||
github.com/gofiber/swagger v0.1.12
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.6
|
||||
github.com/jeremywohl/flatten v1.0.1
|
||||
github.com/knadh/koanf v1.5.0
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230205110531-05840c74e63c
|
||||
github.com/microcosm-cc/bluemonday v1.0.23
|
||||
github.com/nikoksr/notify v0.38.1
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230731060429-6ed8bf011875
|
||||
github.com/microcosm-cc/bluemonday v1.0.25
|
||||
github.com/nikoksr/notify v0.41.0
|
||||
github.com/qwqcode/go-aliyun-email v0.0.0-20180120030821-cb6e7b1382bf
|
||||
github.com/redis/go-redis/v9 v9.1.0
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
|
||||
github.com/samber/lo v1.37.0
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/samber/lo v1.38.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/steambap/captcha v1.4.1
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/swaggo/swag v1.8.10
|
||||
github.com/tidwall/gjson v1.14.4
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/swaggo/swag v1.16.1
|
||||
github.com/tidwall/gjson v1.16.0
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
github.com/yuin/goldmark v1.5.4
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/term v0.6.0
|
||||
github.com/yuin/goldmark v1.5.6
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/term v0.11.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/driver/mysql v1.4.7
|
||||
gorm.io/driver/postgres v1.5.0
|
||||
gorm.io/driver/sqlite v1.4.4
|
||||
gorm.io/driver/sqlserver v1.4.2
|
||||
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11
|
||||
gorm.io/driver/mysql v1.5.1
|
||||
gorm.io/driver/postgres v1.5.2
|
||||
gorm.io/driver/sqlite v1.5.3
|
||||
gorm.io/driver/sqlserver v1.5.1
|
||||
gorm.io/gorm v1.25.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ClickHouse/ch-go v0.58.2 // indirect
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.13.2 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
@@ -59,15 +62,15 @@ require (
|
||||
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.12.3 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.6.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/spec v0.20.8 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||
github.com/go-openapi/spec v0.20.9 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
|
||||
github.com/golang-sql/sqlexp v0.1.0 // indirect
|
||||
@@ -83,56 +86,58 @@ require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v4 v4.18.1 // indirect
|
||||
github.com/jackc/pgx/v5 v5.3.1 // indirect
|
||||
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.16.3 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/microsoft/go-mssqldb v0.20.0 // indirect
|
||||
github.com/microsoft/go-mssqldb v1.5.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/onsi/gomega v1.27.4 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.27.10 // indirect
|
||||
github.com/paulmach/orb v0.10.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.18 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.42.0 // indirect
|
||||
github.com/prometheus/procfs v0.9.0 // indirect
|
||||
github.com/prometheus/client_golang v1.16.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/slack-go/slack v0.12.1 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/slack-go/slack v0.12.2 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/swaggo/files v1.0.0 // indirect
|
||||
github.com/stretchr/objx v0.5.1 // indirect
|
||||
github.com/swaggo/files/v2 v2.0.0 // indirect
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 // indirect
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||
github.com/utahta/go-linenotify v0.5.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.45.0 // indirect
|
||||
github.com/valyala/fasthttp v1.48.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
golang.org/x/image v0.6.0 // indirect
|
||||
golang.org/x/net v0.8.0 // indirect
|
||||
golang.org/x/oauth2 v0.6.0 // indirect
|
||||
golang.org/x/sys v0.6.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
go.opentelemetry.io/otel v1.16.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.16.0 // indirect
|
||||
golang.org/x/image v0.11.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/oauth2 v0.11.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
)
|
||||
@@ -1,17 +1,16 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/ClickHouse/ch-go v0.58.2 h1:jSm2szHbT9MCAB1rJ3WuCJqmGLi5UTjlNu+f530UTS0=
|
||||
github.com/ClickHouse/ch-go v0.58.2/go.mod h1:Ap/0bEmiLa14gYjCiRkYGbXvbe8vwdrfTYWhsuQ99aw=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.13.2 h1:LSg6670+xbd5VczO5Ei3DHZBIeGulfwhNuHCUth/qOA=
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.13.2/go.mod h1:4QITCrdY/ugPYA+QGnJ92h+v7TGaZQ7l0393Q/wlM3Q=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
|
||||
@@ -23,7 +22,6 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
|
||||
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
@@ -56,25 +54,25 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19 h1:pamuM2sgLJLoMWfchc6y071z8ifalajU7btZmZNhoH4=
|
||||
github.com/blinkbean/dingtalk v0.0.0-20210905093040-7d935c0f7e19/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746 h1:wAIE/kN63Oig1DdOzN7O+k4AbFh2cCJoKMFXrwRJtzk=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230124162541-5f7a7d875746/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285 h1:Dr+ezPI5ivhMn/3WOoB86XzMhie146DNaBbhaQWZHMY=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230611145640-acc696258285/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0=
|
||||
github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cheggaaa/pb/v3 v3.1.2 h1:FIxT3ZjOj9XJl0U4o2XbEhjFfZl7jCVCDOGq1ZAB7wQ=
|
||||
github.com/cheggaaa/pb/v3 v3.1.2/go.mod h1:SNjnd0yKcW+kw0brSusraeDd5Bf1zBfxAzTL2ss3yQ4=
|
||||
github.com/cheggaaa/pb/v3 v3.1.4 h1:DN8j4TVVdKu3WxVwcRKu0sG00IIU6FewoABZzXbRQeo=
|
||||
github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4iUXmSA=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -86,14 +84,14 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu
|
||||
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
|
||||
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eko/gocache/lib/v4 v4.1.3 h1:i7xVxWWhAESLAcHgp73piJtST63TS7ThFD7LE78vmXc=
|
||||
github.com/eko/gocache/lib/v4 v4.1.3/go.mod h1:7uSUlaDeRgr17IYCV8kSluhRDXq4SGutWWxTpxpwW3w=
|
||||
github.com/eko/gocache/store/bigcache/v4 v4.1.2 h1:8uMDpgxTG7BvyLHBFqL3Ao809bVrXfrWqo7v6ALiwTw=
|
||||
github.com/eko/gocache/store/bigcache/v4 v4.1.2/go.mod h1:Lut5Sk/yI835w02tmwx4ecezYQo445L5sdICsk1zgho=
|
||||
github.com/eko/gocache/store/memcache/v4 v4.1.2 h1:ke3OolL6a/fgoSAvuRD42q6Q/80hDnbDrZeIjjguDlg=
|
||||
github.com/eko/gocache/store/memcache/v4 v4.1.2/go.mod h1:0raU4CSQgzihTahA4dxrxsafHt8j2NH1ATL33lSDpX4=
|
||||
github.com/eko/gocache/store/redis/v4 v4.1.2 h1:PtpjQu8Q698kpC3H06As5As15s+hJofV8NtkddVX5aQ=
|
||||
github.com/eko/gocache/store/redis/v4 v4.1.2/go.mod h1:P2HeqvNwqkdwajYro+qwBWquhdYpN0LgfH2rJ3jK7yg=
|
||||
github.com/eko/gocache/lib/v4 v4.1.4 h1:09mjQwg9iKm0D9ue58LRCeMFxCemdL/Cn1zEzQJopA4=
|
||||
github.com/eko/gocache/lib/v4 v4.1.4/go.mod h1:7uSUlaDeRgr17IYCV8kSluhRDXq4SGutWWxTpxpwW3w=
|
||||
github.com/eko/gocache/store/bigcache/v4 v4.2.0 h1:oWPW/lPmGe2V91iQkSUa0QcWqIIH9U058hKhG0mnsOk=
|
||||
github.com/eko/gocache/store/bigcache/v4 v4.2.0/go.mod h1:lAFv7vJHo9Oy5a6u9uVFmoUZKtH9H/KkH2mzy3Al1Xo=
|
||||
github.com/eko/gocache/store/memcache/v4 v4.2.0 h1:yG+SkTfFLLqO3pWr5gj++fWWy/1KjWg//OThAFavJeQ=
|
||||
github.com/eko/gocache/store/memcache/v4 v4.2.0/go.mod h1:LM41TOGwHXOs48p1iwP9VUWd7ClX3HHvFLFeIuQBkjI=
|
||||
github.com/eko/gocache/store/redis/v4 v4.2.0 h1:YguIDZuMeVFlQFi0DiYQpL7NvHAckinYFjWxvr0qmLw=
|
||||
github.com/eko/gocache/store/redis/v4 v4.2.0/go.mod h1:IP4GWCjca/pyruK5GweTZ7zzoFlgQABd3cVMJFEgHfs=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -109,6 +107,10 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
|
||||
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
|
||||
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
|
||||
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
@@ -118,44 +120,45 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
|
||||
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/spec v0.20.8 h1:ubHmXNY3FCIOinT8RNrrPfGc9t7I1qhPtdOGoG2AxRU=
|
||||
github.com/go-openapi/spec v0.20.8/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
|
||||
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc h1:jZY+lpZB92nvBo2f31oPC/ivGll6NcsnEOORm8Fkr4M=
|
||||
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc/go.mod h1:25mL1NKxbJhB63ihiK8MnNeTRd+xAizd6bOdydrTLUQ=
|
||||
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
|
||||
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.8.1 h1:uonwvepqRvSgddcrReZQhojTlWlmOlHkYAb9ZaOMWgU=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.8.1/go.mod h1:Kdu7YeMC0KRXVHdaQ91Vmx3pcjoTF63h4f1qTJDdXLA=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.9.0 h1:938g5V+GWLVejm3Hc+nWCuEXRlcglZDDlN/t1gWzcSY=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.9.0/go.mod h1:cdsKD2ApFBjdog9jRsz6EJqF+LClq/hrwE9K/1Dzo4s=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/fiber/v2 v2.42.0 h1:Fnp7ybWvS+sjNQsFvkhf4G8OhXswvB6Vee8hM/LyS+8=
|
||||
github.com/gofiber/fiber/v2 v2.42.0/go.mod h1:3+SGNjqMh5VQH5Vz2Wdi43zTIV16ktlFd3x3R6O1Zlc=
|
||||
github.com/gofiber/swagger v0.1.9 h1:JcUVtxa9cOQdQ0DdLwTA0u2QyM5d2/D/3fUZqBGpYR4=
|
||||
github.com/gofiber/swagger v0.1.9/go.mod h1:IBHyqGmqbfOwbZmt2X5it5m6PfgtB05VjMN3zfRmY1Y=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofiber/fiber/v2 v2.46.0/go.mod h1:DNl0/c37WLe0g92U6lx1VMQuxGUQY5V7EIaVoEsUffc=
|
||||
github.com/gofiber/fiber/v2 v2.48.0 h1:cRVMCb9aUJDsyHxGFLwz/sGzDggdailZZyptU9F9cU0=
|
||||
github.com/gofiber/fiber/v2 v2.48.0/go.mod h1:xqJgfqrc23FJuqGOW6DVgi3HyZEm2Mn9pRqUb2kHSX8=
|
||||
github.com/gofiber/swagger v0.1.12 h1:1Son/Nc1teiIftsVu6UHqXnJ3uf31pUzZO6XQDx3QYs=
|
||||
github.com/gofiber/swagger v0.1.12/go.mod h1:iOCNEt1gNTtlvCEKoxYX4agnZNtxlAjhujMKG6pmG74=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
@@ -170,7 +173,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
@@ -196,15 +198,14 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v0.0.0-20171113160352-8c31c18f31ed/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -239,6 +240,7 @@ github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdv
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@@ -257,67 +259,31 @@ github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEF
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7VjIE6z8dIvMsI4/s+1qr5EL+zoIGev1BQj1eoJ8=
|
||||
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
|
||||
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
|
||||
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
|
||||
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
|
||||
github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
|
||||
github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
||||
github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU=
|
||||
github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
||||
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw=
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
|
||||
@@ -325,14 +291,15 @@ github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbg
|
||||
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
@@ -343,73 +310,67 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
|
||||
github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
|
||||
github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible h1:Uf9/OxV0zCVfqyvwZPH8CrdiHXXmMRa/L91G3btQblQ=
|
||||
github.com/line/line-bot-sdk-go v7.8.0+incompatible/go.mod h1:0RjLjJEAU/3GIcHkC3av6O4jInAbt25nnZVmOFUgDBg=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230205110531-05840c74e63c h1:s1xqzuKdzMJlCIh3TD36lZKGfLD4KT340s/NbrXgRIw=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230205110531-05840c74e63c/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230731060429-6ed8bf011875 h1:SeCWAUH2JKORWw0q0/kic8PiHXf6KfADToUQYGHyA2I=
|
||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230731060429-6ed8bf011875/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
|
||||
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
|
||||
github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
|
||||
github.com/microsoft/go-mssqldb v0.20.0 h1:jAfvs5TIR311fEuNgohFzMtUO0IcCHyI1kWAVws4ES8=
|
||||
github.com/microsoft/go-mssqldb v0.20.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
|
||||
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
|
||||
github.com/microsoft/go-mssqldb v1.1.0/go.mod h1:LzkFdl4z2Ck+Hi+ycGOTbL56VEfgoyA2DvYejrNGbRk=
|
||||
github.com/microsoft/go-mssqldb v1.5.0 h1:CgENxkwtOBNj3Jg6T1X209y2blCfTTcwuOlznd2k9fk=
|
||||
github.com/microsoft/go-mssqldb v1.5.0/go.mod h1:lmWsjHD8XX/Txr0f8ZqgbEZSC+BZjmEQy/Ms+rLrvho=
|
||||
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
@@ -433,30 +394,39 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
|
||||
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nikoksr/notify v0.38.1 h1:+WjA3nUMMhfxKuFFYmTIgDOykdI7GPP3ZWWg3SLuQyo=
|
||||
github.com/nikoksr/notify v0.38.1/go.mod h1:BA0LnpzG+iBlnxtPnSmV/Ei57wqEtyv9V9IJ+rDlo58=
|
||||
github.com/nikoksr/notify v0.41.0 h1:4LGE41GpWdHX5M3Xo6DlWRwS2WLDbOq1Rk7IzY4vjmQ=
|
||||
github.com/nikoksr/notify v0.41.0/go.mod h1:FoE0UVPeopz1Vy5nm9vQZ+JVmYjEIjQgbFstbkw+cRE=
|
||||
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
|
||||
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/paulmach/orb v0.10.0 h1:guVYVqzxHE/CQ1KpfGO077TR0ATHSNjp4s6XGLn3W9s=
|
||||
github.com/paulmach/orb v0.10.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
|
||||
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
|
||||
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1-0.20161029093637-248dadf4e906/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -472,27 +442,29 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
|
||||
github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
|
||||
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
|
||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
|
||||
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/qwqcode/go-aliyun-email v0.0.0-20180120030821-cb6e7b1382bf h1:FH9O7I17HCdVYbK1IMxTGgD3bVuTzyO45bR94iE+T/U=
|
||||
github.com/qwqcode/go-aliyun-email v0.0.0-20180120030821-cb6e7b1382bf/go.mod h1:5N3D7E1/M3G95YK4jPjcPRf/A7XBis0ni4VMmcVnnRY=
|
||||
github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
|
||||
github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
|
||||
github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3 h1:iaa+J202f+Nc+A8zi75uccC8Wg3omaM7HDeimXA22Ag=
|
||||
github.com/rhysd/go-github-selfupdate v1.2.3/go.mod h1:mp/N8zj6jFfBQy/XMYoWsmfzxazpPAODuqarmPDe2Rg=
|
||||
@@ -503,50 +475,44 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
|
||||
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 h1:rmMl4fXJhKMNWl+K+r/fq4FbbKI+Ia2m9hYBLm2h4G4=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94/go.mod h1:90zrgN3D/WJsDd1iXHT96alCoN2KJo6/4x1DZC3wZs8=
|
||||
github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d/go.mod h1:Gy+0tqhJvgGlqnTF8CVGP0AaGRjwBtXs/a5PA0Y3+A4=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw=
|
||||
github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
|
||||
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ=
|
||||
github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
|
||||
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/steambap/captcha v1.4.1 h1:OmMdxLCWCqJvsFaFYwRpvMckIuvI6s8s1LsBrBw97P0=
|
||||
github.com/steambap/captcha v1.4.1/go.mod h1:oC9T7IfEgnrhzjDz5Djf1H7GPffCzRMbsQfFkJmhlnk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
|
||||
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@@ -557,26 +523,26 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
|
||||
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
|
||||
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
|
||||
github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
|
||||
github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
|
||||
github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=
|
||||
github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2 h1:iiDhRitByXAEyjgBqsKi9QU4o2TNtv9kPP3RgPgXBPw=
|
||||
github.com/tcnksm/go-gitconfig v0.1.2/go.mod h1:/8EhP4H7oJZdIPyT+/UIsG87kTzrzM4UsLGSItWYCpE=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
|
||||
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
|
||||
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
|
||||
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
|
||||
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
|
||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
@@ -586,73 +552,59 @@ github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem
|
||||
github.com/utahta/go-linenotify v0.5.0/go.mod h1:KsvBXil2wx+ByaCR0e+IZKTbp4pDesc7yjzRigLf6pE=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.44.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
|
||||
github.com/valyala/fasthttp v1.45.0 h1:zPkkzpIn8tdHZUrVa6PzYd0i5verqiPSkgTd3bSUcpA=
|
||||
github.com/valyala/fasthttp v1.45.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc=
|
||||
github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
|
||||
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/yuin/goldmark v1.5.6 h1:COmQAWTCcGetChm3Ig7G/t8AFAN00t+o8Mt4cf7JpwA=
|
||||
github.com/yuin/goldmark v1.5.6/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
|
||||
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
|
||||
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
|
||||
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
|
||||
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo=
|
||||
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ=
|
||||
golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4=
|
||||
golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0=
|
||||
golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
|
||||
golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -660,7 +612,9 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20171115151908-9dfe39835686/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -673,35 +627,32 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
|
||||
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
|
||||
golang.org/x/sync v0.0.0-20171101214715-fd80eb99c8f6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -713,8 +664,9 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -726,12 +678,13 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -741,6 +694,7 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -751,68 +705,61 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E=
|
||||
golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -847,8 +794,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||
@@ -859,12 +807,9 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU=
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
@@ -883,19 +828,18 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
|
||||
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
|
||||
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
|
||||
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
|
||||
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
|
||||
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/driver/sqlserver v1.4.2 h1:nMtEeKqv2R/vv9FoHUFWfXfP6SskAgRar0TPlZV1stk=
|
||||
gorm.io/driver/sqlserver v1.4.2/go.mod h1:XHwBuB4Tlh7DqO0x7Ema8dmyWsQW7wi38VQOAFkrbXY=
|
||||
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
|
||||
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g=
|
||||
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
|
||||
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
|
||||
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
|
||||
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
|
||||
gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g=
|
||||
gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
||||
gorm.io/driver/sqlserver v1.5.1 h1:wpyW/pR26U94uaujltiFGXY7fd2Jw5hC9PB1ZF/Y5s4=
|
||||
gorm.io/driver/sqlserver v1.5.1/go.mod h1:AYHzzte2msKTmYBYsSIq8ZUsznLJwBdkB2wpI+kt0nM=
|
||||
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw=
|
||||
gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
|
||||
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
|
||||
@@ -1,7 +1,6 @@
|
||||
package anti_spam
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -9,33 +8,31 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/config"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/ArtalkJS/Artalk/internal/log"
|
||||
)
|
||||
|
||||
type AkismetParams struct {
|
||||
Blog string `name:"blog"` // required
|
||||
UserIP string `name:"user_ip"` // required
|
||||
UserAgent string `name:"user_agent"` // required
|
||||
var _ Checker = (*AkismetChecker)(nil)
|
||||
|
||||
CommentType string `name:"comment_type"`
|
||||
CommentAuthor string `name:"comment_author"`
|
||||
CommentAuthorEmail string `name:"comment_author_email"`
|
||||
CommentAuthorURL string `name:"comment_author_url"`
|
||||
CommentContent string `name:"comment_content"`
|
||||
|
||||
UserRole string `name:"user_role"`
|
||||
Referrer string `name:"referrer"`
|
||||
Permalink string `name:"permalink"`
|
||||
BlogLang string `name:"blog_lang"`
|
||||
BlogCharset string `name:"blog_charset"`
|
||||
type AkismetChecker struct {
|
||||
key string
|
||||
}
|
||||
|
||||
// @link https://akismet.com/development/api/#comment-check
|
||||
func Akismet(params *AkismetParams, key string) (isPass bool, err error) {
|
||||
func NewAkismetChecker(key string) Checker {
|
||||
return &AkismetChecker{
|
||||
key: key,
|
||||
}
|
||||
}
|
||||
|
||||
func (*AkismetChecker) Name() string {
|
||||
return "akismet"
|
||||
}
|
||||
|
||||
func (c *AkismetChecker) Check(p *CheckerParams) (bool, error) {
|
||||
// @link https://akismet.com/development/api/#comment-check
|
||||
form := url.Values{}
|
||||
|
||||
v := reflect.ValueOf(*params)
|
||||
reqParams := newAkismetReqParams(p)
|
||||
v := reflect.ValueOf(*reqParams)
|
||||
t := v.Type()
|
||||
for i := 0; i < v.Type().NumField(); i++ {
|
||||
if v.Field(i).String() != "" {
|
||||
@@ -46,7 +43,7 @@ func Akismet(params *AkismetParams, key string) (isPass bool, err error) {
|
||||
client := &http.Client{}
|
||||
|
||||
reqBody := strings.NewReader(form.Encode())
|
||||
api := fmt.Sprintf("https://%s.rest.akismet.com/1.1/comment-check", key)
|
||||
api := fmt.Sprintf("https://%s.rest.akismet.com/1.1/comment-check", c.key)
|
||||
req, err := http.NewRequest("POST", api, reqBody)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -67,20 +64,47 @@ func Akismet(params *AkismetParams, key string) (isPass bool, err error) {
|
||||
|
||||
respStr := string(respBody)
|
||||
|
||||
if config.Instance.Debug {
|
||||
logrus.Info("akismet Spam Detection Response ", respStr)
|
||||
}
|
||||
log.Debug("akismet Spam Detection Response ", respStr)
|
||||
|
||||
switch respStr {
|
||||
case "true":
|
||||
// is a spam comment
|
||||
isPass = false
|
||||
return isPass, nil
|
||||
return false, nil
|
||||
case "false":
|
||||
// not a spam comment
|
||||
isPass = true
|
||||
return isPass, nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, errors.New(respStr)
|
||||
return false, fmt.Errorf(respStr)
|
||||
}
|
||||
|
||||
type AkismetReqParams struct {
|
||||
Blog string `name:"blog"` // required
|
||||
UserIP string `name:"user_ip"` // required
|
||||
UserAgent string `name:"user_agent"` // required
|
||||
|
||||
CommentType string `name:"comment_type"`
|
||||
CommentAuthor string `name:"comment_author"`
|
||||
CommentAuthorEmail string `name:"comment_author_email"`
|
||||
CommentAuthorURL string `name:"comment_author_url"`
|
||||
CommentContent string `name:"comment_content"`
|
||||
|
||||
UserRole string `name:"user_role"`
|
||||
Referrer string `name:"referrer"`
|
||||
Permalink string `name:"permalink"`
|
||||
BlogLang string `name:"blog_lang"`
|
||||
BlogCharset string `name:"blog_charset"`
|
||||
}
|
||||
|
||||
func newAkismetReqParams(params *CheckerParams) *AkismetReqParams {
|
||||
return &AkismetReqParams{
|
||||
Blog: params.BlogURL,
|
||||
|
||||
UserIP: params.UserIP,
|
||||
UserAgent: params.UserAgent,
|
||||
|
||||
CommentAuthor: params.UserName,
|
||||
CommentAuthorEmail: params.UserEmail,
|
||||
CommentContent: params.Content,
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package anti_spam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/config"
|
||||
"github.com/ArtalkJS/Artalk/internal/entity"
|
||||
"github.com/ArtalkJS/Artalk/internal/query"
|
||||
"github.com/ArtalkJS/Artalk/internal/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var AntiSpamReplaceKeywords *[]string
|
||||
|
||||
const LOG_TAG = "[Spam Interception] "
|
||||
|
||||
func SyncSpamCheck(comment *entity.Comment, fiberCtx *fiber.Ctx) {
|
||||
// 拦截评论
|
||||
BlockCommentBy := func(blocker string) {
|
||||
logrus.Info(fmt.Sprintf(LOG_TAG+"%s Successful blocking of comments ID=%d CONT=%s", blocker, comment.ID, strconv.Quote(comment.Content)))
|
||||
if comment.IsPending {
|
||||
return
|
||||
}
|
||||
comment.IsPending = true // 改为待审状态
|
||||
query.UpdateComment(comment)
|
||||
}
|
||||
|
||||
// 拦截失败处理
|
||||
BlockFailBy := func(blocker string, err error) {
|
||||
logrus.Error(fmt.Sprintf(LOG_TAG+"%s Interception error occurred ID=%d Err: %s", blocker, comment.ID, strconv.Quote(comment.Content)), err)
|
||||
}
|
||||
|
||||
// 统一拦截处理
|
||||
ApiCommonHandle := func(blocker string, isPass bool, err error) {
|
||||
// ApiFailBlock mode
|
||||
isApiFailBlock := config.Instance.Moderator.ApiFailBlock
|
||||
|
||||
if err != nil {
|
||||
// Api 发生错误
|
||||
BlockFailBy(blocker, err) // 报告错误
|
||||
if isApiFailBlock {
|
||||
BlockCommentBy(blocker) // 仍然拦截
|
||||
}
|
||||
} else if !isPass {
|
||||
// Api 未发生错误,并且 not pass
|
||||
BlockCommentBy(blocker) // 拦截评论
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare data for Spam-Check
|
||||
user := query.FetchUserForComment(comment)
|
||||
siteURL := ""
|
||||
if comment.SiteName != "" {
|
||||
site := query.FindSite(comment.SiteName)
|
||||
siteURL = query.CookSite(&site).FirstUrl
|
||||
}
|
||||
if siteURL == "" { // 从 referer 中提取网站
|
||||
if pr, err := url.Parse(string(fiberCtx.Request().Header.Referer())); err == nil && pr.Scheme != "" && pr.Host != "" {
|
||||
siteURL = fmt.Sprintf("%s://%s", pr.Scheme, pr.Host)
|
||||
}
|
||||
}
|
||||
|
||||
// Akismet
|
||||
akismetKey := strings.TrimSpace(config.Instance.Moderator.AkismetKey)
|
||||
if akismetKey != "" {
|
||||
isPass, err := Akismet(&AkismetParams{
|
||||
Blog: siteURL,
|
||||
|
||||
UserIP: fiberCtx.IP(),
|
||||
UserAgent: string(fiberCtx.Request().Header.UserAgent()),
|
||||
|
||||
CommentType: "comment",
|
||||
CommentAuthor: user.Name,
|
||||
CommentAuthorEmail: user.Email,
|
||||
CommentContent: comment.Content,
|
||||
}, akismetKey)
|
||||
|
||||
ApiCommonHandle("Akismet", isPass, err)
|
||||
}
|
||||
|
||||
// 腾讯云
|
||||
tencentConf := config.Instance.Moderator.Tencent
|
||||
if tencentConf.Enabled {
|
||||
isPass, err := Tencent(TencentParams{
|
||||
SecretID: tencentConf.SecretID,
|
||||
SecretKey: tencentConf.SecretKey,
|
||||
Region: tencentConf.Region,
|
||||
|
||||
Content: comment.Content,
|
||||
CommentID: comment.ID,
|
||||
UserID: comment.UserID,
|
||||
UserIP: comment.IP,
|
||||
UserName: user.Name,
|
||||
})
|
||||
|
||||
ApiCommonHandle("腾讯云", isPass, err)
|
||||
}
|
||||
|
||||
// 阿里云
|
||||
aliyunConf := config.Instance.Moderator.Aliyun
|
||||
if aliyunConf.Enabled {
|
||||
isPass, err := Aliyun(AliyunParams{
|
||||
AccessKeyID: aliyunConf.AccessKeyID,
|
||||
AccessKeySecret: aliyunConf.AccessKeySecret,
|
||||
Region: aliyunConf.Region,
|
||||
|
||||
Content: comment.Content,
|
||||
CommentID: comment.ID,
|
||||
})
|
||||
|
||||
ApiCommonHandle("阿里云", isPass, err)
|
||||
}
|
||||
|
||||
// 关键字过滤
|
||||
keywordsConf := config.Instance.Moderator.Keywords
|
||||
if keywordsConf.Enabled {
|
||||
// 懒加载,初始化
|
||||
if AntiSpamReplaceKeywords == nil {
|
||||
AntiSpamReplaceKeywords = &[]string{}
|
||||
// 加载文件
|
||||
for _, f := range keywordsConf.Files {
|
||||
buf, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
logrus.Error("Failed to load Keyword Dictionary file:" + f)
|
||||
} else {
|
||||
fileContent := string(buf)
|
||||
*AntiSpamReplaceKeywords = append(*AntiSpamReplaceKeywords, utils.SplitAndTrimSpace(fileContent, keywordsConf.FileSep)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 关键词过滤
|
||||
handleContent := comment.Content
|
||||
replaced := false
|
||||
for _, keyword := range *AntiSpamReplaceKeywords {
|
||||
if strings.Contains(handleContent, keyword) {
|
||||
if keywordsConf.Pending {
|
||||
BlockCommentBy("Keyword")
|
||||
break
|
||||
}
|
||||
|
||||
if keywordsConf.ReplacTo != "" {
|
||||
handleContent = strings.Replace(handleContent, keyword, strings.Repeat(keywordsConf.ReplacTo, len([]rune(keyword))), -1)
|
||||
replaced = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !keywordsConf.Pending && replaced && keywordsConf.ReplacTo != "" {
|
||||
logrus.Info(fmt.Sprintf(LOG_TAG+"Keyword Replacement Comments ID=%d Original=%s Processed=%s", comment.ID, strconv.Quote(comment.Content), strconv.Quote(handleContent)))
|
||||
|
||||
// 保存评论
|
||||
comment.Content = handleContent
|
||||
query.UpdateComment(comment)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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),
|
||||
@@ -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)}))
|
||||
}
|
||||
@@ -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": ¶ms.TargetSiteName,
|
||||
"t_url": ¶ms.TargetSiteUrl,
|
||||
"t_url_resolver": ¶ms.UrlResolver,
|
||||
"json_file": ¶ms.JsonFile,
|
||||
"json_data": ¶ms.JsonData,
|
||||
"assumeyes": ¶ms.Assumeyes,
|
||||
})
|
||||
|
||||
return ¶ms
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Vendored
+90
@@ -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
|
||||
}
|
||||
Vendored
+147
@@ -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()
|
||||
})
|
||||
}
|
||||
Vendored
-209
@@ -1,209 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/config"
|
||||
"github.com/ArtalkJS/Artalk/internal/entity"
|
||||
"github.com/eko/gocache/lib/v4/store"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
var (
|
||||
CacheFindGroup = new(singleflight.Group)
|
||||
)
|
||||
|
||||
func FindAndStoreCache(name string, dest interface{}, queryDBResult func() interface{}) error {
|
||||
// SingleFlight 防止缓存击穿 (Cache breakdown)
|
||||
v, err, _ := CacheFindGroup.Do(name, func() (interface{}, error) {
|
||||
err := FindCache(name, dest)
|
||||
|
||||
// cache hit 直接返回结果
|
||||
if err == nil {
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// cache miss 查数据库
|
||||
result := queryDBResult()
|
||||
if err := StoreCache(name, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v != nil {
|
||||
reflect.ValueOf(dest).Elem().Set(reflect.ValueOf(v).Elem()) // similar to `*dest = &v`
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func FindCache(name string, dest interface{}) error {
|
||||
if !config.Instance.Cache.Enabled {
|
||||
return errors.New("cache disabled")
|
||||
}
|
||||
|
||||
// `Get()` is Thread Safe, so no need to add Mutex
|
||||
// @see https://github.com/go-redis/redis/issues/23
|
||||
_, err := CACHE.Get(Ctx, name, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debug("[Cache Hit] " + name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func StoreCache(name string, source interface{}) error {
|
||||
if !config.Instance.Cache.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// `Set()` is Thread Safe too, no need to add Mutex either
|
||||
err := CACHE.Set(Ctx, name, source,
|
||||
store.WithExpiration(time.Duration(config.Instance.Cache.GetExpiresTime())),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debug("[写入缓存] " + name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DelCache(name string) error {
|
||||
if !config.Instance.Cache.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
return CACHE.Delete(Ctx, name)
|
||||
}
|
||||
|
||||
func UserCacheSave(user *entity.User) error {
|
||||
// 缓存 ID
|
||||
err := StoreCache(fmt.Sprintf("user#id=%d", user.ID), user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 缓存 Name x Email
|
||||
err = StoreCache(fmt.Sprintf("user#name=%s;email=%s", strings.ToLower(user.Name), strings.ToLower(user.Email)), user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func UserCacheDel(user *entity.User) {
|
||||
DelCache(fmt.Sprintf("user#id=%d", user.ID))
|
||||
DelCache(fmt.Sprintf("user#name=%s;email=%s", strings.ToLower(user.Name), strings.ToLower(user.Email)))
|
||||
}
|
||||
|
||||
func SiteCacheSave(site *entity.Site) error {
|
||||
// 缓存 ID
|
||||
err := StoreCache(fmt.Sprintf("site#id=%d", site.ID), site)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 缓存 Name
|
||||
err = StoreCache(fmt.Sprintf("site#name=%s", site.Name), site)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func SiteCacheDel(site *entity.Site) {
|
||||
DelCache(fmt.Sprintf("site#id=%d", site.ID))
|
||||
DelCache(fmt.Sprintf("site#name=%s", site.Name))
|
||||
}
|
||||
|
||||
func PageCacheSave(page *entity.Page) error {
|
||||
// 缓存 ID
|
||||
err := StoreCache(fmt.Sprintf("page#id=%d", page.ID), page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 缓存 Key x SiteName
|
||||
err = StoreCache(fmt.Sprintf("page#key=%s;site_name=%s", page.Key, page.SiteName), page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func PageCacheDel(page *entity.Page) {
|
||||
DelCache(fmt.Sprintf("page#id=%d", page.ID))
|
||||
DelCache(fmt.Sprintf("page#key=%s;site_name=%s", page.Key, page.SiteName))
|
||||
}
|
||||
|
||||
func CommentCacheSave(comment *entity.Comment) error {
|
||||
// 缓存 ID
|
||||
err := StoreCache(fmt.Sprintf("comment#id=%d", comment.ID), comment)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 缓存 Rid
|
||||
if comment.Rid != 0 {
|
||||
ChildCommentCacheSave(comment.Rid, comment.ID)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func CommentCacheDel(comment *entity.Comment) {
|
||||
DelCache(fmt.Sprintf("comment#id=%d", comment.ID))
|
||||
|
||||
// 清除 Rid 缓存
|
||||
ChildCommentCacheDel(comment.ID)
|
||||
if comment.Rid != 0 {
|
||||
ChildCommentCacheDel(comment.Rid)
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存 父ID=>子ID 评论数据
|
||||
func ChildCommentCacheSave(parentID uint, childID uint) {
|
||||
var childIDs []uint
|
||||
var cacheName = fmt.Sprintf("parent-comments#pid=%d", parentID)
|
||||
|
||||
err := FindCache(cacheName, &childIDs)
|
||||
if err != nil { // 初始化
|
||||
childIDs = []uint{}
|
||||
}
|
||||
|
||||
isExist := false
|
||||
for _, i := range childIDs {
|
||||
if i == childID {
|
||||
isExist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// append if Not exist
|
||||
if !isExist {
|
||||
childIDs = append(childIDs, childID)
|
||||
}
|
||||
|
||||
StoreCache(cacheName, childIDs)
|
||||
}
|
||||
|
||||
func ChildCommentCacheDel(parentID uint) {
|
||||
DelCache(fmt.Sprintf("parent-comments#pid=%d", parentID))
|
||||
}
|
||||
+40
-25
@@ -2,82 +2,97 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/config"
|
||||
"github.com/allegro/bigcache/v3"
|
||||
"github.com/bradfitz/gomemcache/memcache"
|
||||
"github.com/eko/gocache/lib/v4/cache"
|
||||
lib_cache "github.com/eko/gocache/lib/v4/cache"
|
||||
"github.com/eko/gocache/lib/v4/marshaler"
|
||||
"github.com/eko/gocache/lib/v4/store"
|
||||
bigcache_store "github.com/eko/gocache/store/bigcache/v4"
|
||||
memcache_store "github.com/eko/gocache/store/memcache/v4"
|
||||
redis_store "github.com/eko/gocache/store/redis/v4"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
CACHE *marshaler.Marshaler
|
||||
)
|
||||
type Cache struct {
|
||||
ttl time.Duration
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
instance *lib_cache.Cache[any]
|
||||
marshal *marshaler.Marshaler
|
||||
}
|
||||
|
||||
var Ctx = context.Background()
|
||||
func (cache *Cache) Close() {
|
||||
cache.cancel()
|
||||
cache.marshal = nil
|
||||
cache.instance = nil
|
||||
}
|
||||
|
||||
func OpenCache() (err error) {
|
||||
cacheType := config.Instance.Cache.Type
|
||||
func New(conf config.CacheConf) (*Cache, error) {
|
||||
// create new context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
cache := &Cache{
|
||||
ttl: time.Duration(conf.GetExpiresTime()),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
var cacheStore store.StoreInterface
|
||||
|
||||
switch cacheType {
|
||||
switch conf.Type {
|
||||
|
||||
case config.CacheTypeBuiltin:
|
||||
// 内建缓存
|
||||
bigcacheClient, err := bigcache.NewBigCache(bigcache.DefaultConfig(
|
||||
bigcacheClient, err := bigcache.New(context.Background(), bigcache.DefaultConfig(
|
||||
// Tip: 内建缓存过期时间是一样的,只有 Redis/Memcache 才能设置单个 item 的
|
||||
time.Duration(config.Instance.Cache.GetExpiresTime()),
|
||||
cache.ttl,
|
||||
))
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
cacheStore = bigcache_store.NewBigcache(bigcacheClient) // No options provided (as second argument)
|
||||
|
||||
case config.CacheTypeRedis:
|
||||
// Redis
|
||||
network := "tcp"
|
||||
if config.Instance.Cache.Redis.Network != "" {
|
||||
network = config.Instance.Cache.Redis.Network
|
||||
if conf.Redis.Network != "" {
|
||||
network = conf.Redis.Network
|
||||
}
|
||||
|
||||
cacheStore = redis_store.NewRedis(redis.NewClient(&redis.Options{
|
||||
Network: network,
|
||||
Addr: config.Instance.Cache.Server,
|
||||
Username: config.Instance.Cache.Redis.Username,
|
||||
Password: config.Instance.Cache.Redis.Password,
|
||||
DB: config.Instance.Cache.Redis.DB,
|
||||
Addr: conf.Server,
|
||||
Username: conf.Redis.Username,
|
||||
Password: conf.Redis.Password,
|
||||
DB: conf.Redis.DB,
|
||||
}))
|
||||
|
||||
case config.CacheTypeMemcache:
|
||||
// Memcache
|
||||
servers := strings.Split(config.Instance.Cache.Server, ",")
|
||||
servers := strings.Split(conf.Server, ",")
|
||||
cacheStore = memcache_store.NewMemcache(
|
||||
memcache.New(servers...),
|
||||
store.WithExpiration(time.Duration(config.Instance.Cache.GetExpiresTime())),
|
||||
store.WithExpiration(cache.ttl),
|
||||
)
|
||||
|
||||
default:
|
||||
logrus.Fatal("Invalid cache type `" + cacheType + "`, please check config option `cache.type`")
|
||||
log.Fatal(`Invalid cache type "` + conf.Type + `", please check config option "cache.type"`)
|
||||
|
||||
}
|
||||
|
||||
cacheInstance := cache.New[any](cacheStore)
|
||||
cache.instance = lib_cache.New[any](cacheStore)
|
||||
|
||||
// marshaler wrapper
|
||||
// marshaler using VmihailencoMsgpack
|
||||
// @link https://github.com/vmihailenco/msgpack
|
||||
// Benchmarks
|
||||
// @link https://github.com/alecthomas/go_serialization_benchmarks
|
||||
CACHE = marshaler.New(cacheInstance)
|
||||
cache.marshal = marshaler.New(cache.instance)
|
||||
|
||||
return
|
||||
return cache, nil
|
||||
}
|
||||
Vendored
+27
@@ -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)
|
||||
}
|
||||
Vendored
-10
@@ -1,10 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"github.com/ArtalkJS/Artalk/internal/db"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func DB() *gorm.DB {
|
||||
return db.DB()
|
||||
}
|
||||
Vendored
-111
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+46
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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"` // 图片存放路径
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"unicode"
|
||||
|
||||
"github.com/ArtalkJS/Artalk/internal/cache"
|
||||
"github.com/ArtalkJS/Artalk/internal/config"
|
||||
"github.com/ArtalkJS/Artalk/internal/db"
|
||||
"github.com/ArtalkJS/Artalk/internal/i18n"
|
||||
"github.com/ArtalkJS/Artalk/internal/ip_region"
|
||||
"github.com/ArtalkJS/Artalk/internal/notify_launcher"
|
||||
|
||||
"github.com/rifflock/lfshook"
|
||||
"github.com/sirupsen/logrus"
|
||||
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||
)
|
||||
|
||||
var firstLoad = true
|
||||
var mutex = sync.Mutex{}
|
||||
|
||||
// 装载核心功能
|
||||
func LoadCore(cfgFile string, workDir string) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
firstLoad = false
|
||||
|
||||
initConfig(cfgFile, workDir)
|
||||
initI18n()
|
||||
initLog()
|
||||
initCache()
|
||||
initDB()
|
||||
notify_launcher.Init() // 初始化 Notify 发射台
|
||||
initIPRegion()
|
||||
|
||||
// 首次 Load
|
||||
if firstLoad {
|
||||
// 缓存预热
|
||||
if config.Instance.Cache.Enabled && config.Instance.Cache.WarmUp {
|
||||
cache.CacheWarmUp()
|
||||
}
|
||||
// 异步加载
|
||||
// go func() {
|
||||
// entity.CacheWarmUp()
|
||||
// }()
|
||||
}
|
||||
}
|
||||
|
||||
// 仅装载配置
|
||||
func LoadConfOnly(cfgFile string, workDir string) {
|
||||
initConfig(cfgFile, workDir)
|
||||
}
|
||||
|
||||
// 1. 初始化配置
|
||||
func initConfig(cfgFile string, workDir string) {
|
||||
// 切换工作目录
|
||||
if workDir != "" {
|
||||
if err := os.Chdir(workDir); err != nil {
|
||||
logrus.Fatal("Working directory change error: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
if cfgFile == "" {
|
||||
cfgFile = config.DEFAULT_CONF_FILE
|
||||
|
||||
// 默认配置文件名 "artalk-go.yml"(for 向下兼容)
|
||||
if _, err := os.Stat("artalk-go.yml"); err == nil {
|
||||
cfgFile = "artalk-go.yml"
|
||||
}
|
||||
}
|
||||
|
||||
// 自动生成新配置文件
|
||||
if !CheckFileExist(cfgFile) {
|
||||
Gen("config", cfgFile, false)
|
||||
}
|
||||
|
||||
config.Init(cfgFile)
|
||||
}
|
||||
|
||||
func initI18n() {
|
||||
if config.Instance.Locale == "" {
|
||||
config.Instance.Locale = "en"
|
||||
|
||||
// zh-CN default patch
|
||||
// 判断配置文件中是否有中文,若有中文则将 locale 设置为 zh-CN
|
||||
if confRaw, err := os.ReadFile(config.GetCfgFileLoaded()); err == nil {
|
||||
containsHan := false
|
||||
for _, runeValue := range string(confRaw) {
|
||||
if unicode.Is(unicode.Han, runeValue) {
|
||||
containsHan = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if containsHan {
|
||||
config.Instance.Locale = "zh-CN"
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Warn("config `locale` is not set, now it is: " + strconv.Quote(config.Instance.Locale))
|
||||
} else if config.Instance.Locale == "zh" {
|
||||
config.Instance.Locale = "zh-CN"
|
||||
}
|
||||
|
||||
i18n.Init(config.Instance.Locale)
|
||||
}
|
||||
|
||||
// 2. 初始化日志
|
||||
func initLog() {
|
||||
logrus.New()
|
||||
if !config.Instance.Log.Enabled {
|
||||
logrus.SetOutput(io.Discard)
|
||||
return
|
||||
}
|
||||
|
||||
// 命令行输出格式
|
||||
stdFormatter := &prefixed.TextFormatter{
|
||||
DisableTimestamp: true,
|
||||
ForceFormatting: true,
|
||||
ForceColors: true,
|
||||
DisableColors: false,
|
||||
}
|
||||
|
||||
logrus.SetFormatter(stdFormatter)
|
||||
logrus.SetOutput(os.Stdout)
|
||||
|
||||
if config.Instance.Debug {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
} else {
|
||||
logrus.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
// 日志输出到文件
|
||||
if config.Instance.Log.Filename != "" {
|
||||
fileFormatter := &prefixed.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "2006-01-02.15:04:05.000000",
|
||||
ForceFormatting: true,
|
||||
ForceColors: false,
|
||||
DisableColors: true,
|
||||
}
|
||||
|
||||
pathMap := lfshook.PathMap{
|
||||
logrus.InfoLevel: config.Instance.Log.Filename,
|
||||
logrus.DebugLevel: config.Instance.Log.Filename,
|
||||
logrus.ErrorLevel: config.Instance.Log.Filename,
|
||||
}
|
||||
|
||||
newHooks := make(logrus.LevelHooks)
|
||||
newHooks.Add(lfshook.NewHook(
|
||||
pathMap,
|
||||
fileFormatter,
|
||||
))
|
||||
|
||||
//logrus.AddHook(lfshook.NewHook()) // 使用 Replace 而不使用 Add
|
||||
logrus.StandardLogger().ReplaceHooks(newHooks)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 初始化缓存
|
||||
func initCache() {
|
||||
err := cache.OpenCache()
|
||||
if err != nil {
|
||||
logrus.Error("[Cache] ", "Init cache error: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 初始化数据库
|
||||
func initDB() {
|
||||
db.InitDB()
|
||||
db.MigrateModels()
|
||||
SyncFromConf()
|
||||
}
|
||||
|
||||
func initIPRegion() {
|
||||
irConf := &config.Instance.IPRegion
|
||||
if !irConf.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
if irConf.DBPath == "" {
|
||||
irConf.DBPath = "./data/ip2region.xdb"
|
||||
}
|
||||
if irConf.Precision == "" {
|
||||
// 默认精确到省
|
||||
irConf.Precision = string(ip_region.Province)
|
||||
}
|
||||
|
||||
ip_region.Init(irConf.DBPath) // 初始化 IP 归属地查询
|
||||
ip_region.SetPrecision(ip_region.Precision(irConf.Precision)) // 精度设置
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package core
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Event data
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
type BootstrapEvent struct {
|
||||
App *App
|
||||
}
|
||||
|
||||
type TerminateEvent struct {
|
||||
App *App
|
||||
}
|
||||
+14
-28
@@ -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)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package core
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Service interface {
|
||||
Init() error
|
||||
Dispose() error
|
||||
}
|
||||
|
||||
func genServiceName[T any]() string {
|
||||
var t T
|
||||
|
||||
// struct
|
||||
name := fmt.Sprintf("%T", t)
|
||||
if name != "<nil>" {
|
||||
return name
|
||||
}
|
||||
|
||||
// interface
|
||||
return fmt.Sprintf("%T", new(T))
|
||||
}
|
||||
Loaded 100 of 271 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user