Compare commits

..
68 Commits
Author SHA1 Message Date
qwqcode d25beede91 fix(scripts): detect unpublished npm versions
Query the npm registry for the exact local version instead of comparing only with the latest dist-tag.

Report unpublished packages accurately and avoid spawning pnpm for each lookup.
2026-07-25 04:01:48 +08:00
qwqcode 16b690c8c0 fix(plugins): prepare unpublished packages for release
Declare compatible Artalk ranges and npm repository metadata for the pending plugin versions.

Correct ESM and CommonJS declaration exports, trim plugin-kit package contents, and validate package types in CI.
2026-07-25 04:01:43 +08:00
qwqcode 2036a0261e fix(ci): publish npm package with trusted OIDC
Replace the long-lived npm token with GitHub OIDC credentials, verify the release version, and retain pnpm packaging so the published tarball still includes the repository license.
2026-07-25 03:26:34 +08:00
qwqcode 66c8f6b94b fix(docker): push release images by digest
Avoid assigning temporary platform tags to digest-only outputs so Buildx can push canonical per-platform digests before the manifest merge.
2026-07-25 03:26:33 +08:00
qwqcode 1bf21404bd fix(build): bind Docker metadata to release commit
Pass the resolved release commit into Docker builds, verify every checkout against it, and derive image version metadata and sha tags from the packaged source instead of the workflow invocation commit.
2026-07-24 14:24:00 +08:00
qwqcode 9ec0f1c74a fix(ci): pin release retries to reviewed commits
Resolve automatic releases from the merged release PR commit and require an explicit immutable commit when recovering a missing tag. Reuse existing tags idempotently, restore annotated release tags, and reject duplicate release preparation or inconsistent version files.
2026-07-24 14:23:26 +08:00
qwqcode 9b5e47d602 fix(ci): create release tags under tag ruleset
Create lightweight release refs directly at the checked-out commit so the v* linear-history rule evaluates the commit instead of an annotated tag object. Resolve existing tags through the remote API, verify their exact target without overwriting them, and accept an explicit version for safe manual release retries.
2026-07-23 23:52:35 +08:00
github-actions[bot] 03a433d94a chore: release v2.10.0 (#1158)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-07-23 23:41:37 +08:00
qwqcode 4a9b07e687 fix(ci): preserve nightly tag during release refresh
Keep the movable nightly tag while replacing the release, verify its exact commit through the Git refs API, and pass the build SHA as the release target. This avoids the tag visibility race caused by deleting and immediately recreating the tag before --verify-tag.
2026-07-23 14:04:01 +08:00
qwqcode d00b63c0af chore(ci): fix git-chglog installation 2026-07-23 13:20:48 +08:00
qwqcode 97352b583e fix(api/vote): return not found for deleted targets 2026-07-22 22:20:28 +08:00
qwqcode 0b44989e5f fix(cache): return database results on store failure
Treat cache writes as best-effort after a successful database query. Keep database errors authoritative and cover both paths with a failing store.

Fixes #1154
2026-07-22 22:20:28 +08:00
qwqcode a5308548ba fix(config): preserve keyword file separators
Represent control characters with reversible dashboard escapes, keep custom separators intact, and reject empty separators before loading keyword files. Document and test the full YAML round trip.

Fixes #1062
2026-07-22 22:20:28 +08:00
qwqcode 9225fc32f8 fix(email): add Message-ID header
Generate a unique RFC 5322 message identifier for every SMTP and sendmail message, using the sender domain when valid and a safe fallback otherwise.

Fixes #1111
2026-07-22 22:20:27 +08:00
qwqcode 37dc6a68d7 fix(api/stats): exclude pending comments
Use an explicit boolean condition so GORM cannot omit the moderation filter. Cover public comment lists and counts with regression tests.

Fixes #1110
2026-07-22 22:20:27 +08:00
qwqcode 69613beb88 fix(docs): support React 19 useRef types 2026-07-22 22:20:27 +08:00
qwqcode 9c5bd9653f fix(ci): satisfy workflow shell checks
Protect checksum globs and quote test artifact paths for ShellCheck. Pass release versions to Perl through the environment instead of interpolating shell variables into the program text.
2026-07-22 22:20:27 +08:00
qwqcode ca3d37fc2d fix(build): propagate image version metadata
Inject the requested release version and commit hash through Go linker variables so Docker images report the version they were built for, and make release preparation update the linkable version variable.\n\nAssert the version in release image smoke tests and trim unused files from Docker build contexts while preserving both prebuilt-UI and full frontend build paths.
2026-07-22 22:20:27 +08:00
qwqcode f554e6c438 test(ci): expand release validation
Run Go tests natively on Linux AMD64, Linux ARM64, and macOS; reduce the frontend matrix while keeping Node 22, Node 24, and Windows coverage; and make type-entry validation blocking.\n\nAdd the Playwright browser UAT flow to CI, pin test tools, isolate architecture-specific caches and artifacts, and cancel superseded pull-request runs.
2026-07-22 22:20:27 +08:00
qwqcode 76066bbdc0 fix(ci): harden release supply chain
Pin every third-party action to an immutable commit, enforce least-privilege tokens, avoid persisted checkout credentials, and validate all user-controlled values before they reach scripts.\n\nKeep dry-run GoReleaser jobs tokenless, publish npm provenance, generate tags through authenticated GitHub API calls, disable shared caches for release artifacts, and continuously audit workflow syntax and security policy.
2026-07-22 22:20:27 +08:00
qwqcode 3950ef8532 refactor(ci): accelerate nightly publishing
Run AMD64 and ARM64 Docker builds on native GitHub-hosted Linux runners, reuse architecture-scoped BuildKit caches, and merge validated image digests after both builds succeed.\n\nMake manual dry runs build, smoke-test, and upload artifacts without exposing DockerHub credentials or replacing the nightly tag and release.
2026-07-22 22:20:26 +08:00
qwqcode 3f6eb935e4 refactor(ci): optimize multi-architecture releases
Build Docker images once per native AMD64 and ARM64 runner, merge immutable digests into the release manifest, and reuse a single UI artifact across architectures.\n\nKeep dry runs local by separating the source ref from the release version, avoid remote tags and package publication, scope reusable-workflow secrets, and smoke-test each image before publication.
2026-07-22 22:20:26 +08:00
qwqcode 5364d191c7 test(ui): update comment e2e flow
Use the current English development-page labels and the configured base URL. Register the POST response wait before submission to avoid missing fast responses.
2026-07-22 22:20:26 +08:00
qwqcode 5e1b04326b test(ui): isolate Playwright end-to-end specs
Point Playwright directly at tests/e2e so Vitest unit suites are not collected as browser tests. The Chromium project now discovers only the intended comment UAT spec.
2026-07-22 22:20:26 +08:00
qwqcode add379c82c fix(ui/sidebar): handle null array preferences
Treat YAML null values as empty arrays before checking for YAML node serialization. This prevents the settings page from throwing while mounting optional array preferences.
2026-07-22 22:20:26 +08:00
qwqcode d7565e5b33 fix(ui): accept scoped comment lists without page data
Site and user scoped comment responses intentionally omit the page object. Only enforce page data for page-scoped requests and skip page state updates when it is absent.

Add regression coverage for sidebar scopes and the normal page flow.
2026-07-22 22:20:26 +08:00
qwqcode d46326788b docs: document dependency and security updates
Record the backend, frontend, container, and CI dependency refresh in both v2.10.0 release guides.

Explain JWT wire compatibility, the sanitizer replacement, patched transitive pins, generator reproducibility, and unchanged release artifact layout.
2026-07-22 22:20:26 +08:00
qwqcode 729fd9880a fix(ci): harden release tag handling
Pass the event ref through the environment instead of interpolating expression data into the shell, validate the derived semantic version, and quote Git refs and environment files.

Use rev-parse for an exact tag existence check before creating and pushing the release tag.
2026-07-22 22:20:26 +08:00
qwqcode 5b6a453349 fix(ui): preserve generated list response contract
Regenerate the Swagger specification and TypeScript client with the pinned generators, including accurate optional response fields and the verified-email SSO description.

Validate comment-list page data at the API boundary and normalize it back to the public ListData contract before storing or emitting the response.
2026-07-22 22:20:25 +08:00
qwqcode b7b6700407 fix(auth): require verified email for SSO exchange
Reject OIDC userinfo responses without email_verified=true before account lookup or JWT issuance.
2026-07-22 22:20:25 +08:00
qwqcode cb2860a778 docs: add v2.10.0 release notes
BREAKING CHANGE: Beyond #997, #1007, and #1011, v2.10.0 changes default config/data discovery and relative-path semantics; raises source builds to Go 1.26.5; replaces the CGO-backed SQLite driver with a pure-Go implementation; fixes application timezone at process initialization, requiring a full restart to change it; removes the useBackendConf opt-out and remoteConfModifier; changes DI registration, Context mutation, update() return value, and created timing; and requires marked 18 with modern package resolution. Migration guide: https://artalk.js.org/en/guide/releases/v2.10.0.html
2026-07-22 22:20:25 +08:00
qwqcode 705e220a81 test(ui): cover Marked 18 integration 2026-07-22 22:20:25 +08:00
qwqcode ef2d1e8552 test(server): verify plugin URL trust filtering 2026-07-22 22:20:25 +08:00
qwqcode 14e2d9e824 fix(ui): align plugin lifecycle with final config 2026-07-22 22:20:25 +08:00
qwqcode 9dba2ae43f fix(ui): reject direct context replacement 2026-07-22 22:20:25 +08:00
qwqcode f1784a1bd0 fix(ui): restore deprecated context services 2026-07-22 22:20:24 +08:00
qwqcode bc30c4d26d fix(ui/editor): restore deprecated aliases 2026-07-22 22:20:24 +08:00
qwqcode ad9abd643c fix(ui/types): restore deprecated public aliases 2026-07-22 22:20:24 +08:00
qwqcode 2e7a34fc9c fix(ui/types): preserve atomic config values 2026-07-22 22:20:24 +08:00
qwqcode 082e9b7fed fix(core): initialize process timezone once 2026-07-22 22:20:24 +08:00
qwqcode a95c1b9be6 fix(db): enforce Cloud SQL TLS verification 2026-07-22 22:20:24 +08:00
qwqcode dfe65ce621 fix(cmd): preserve config lookup before data chdir 2026-07-22 22:20:24 +08:00
qwqcode da6556ec3c fix(api): make vote updates transactional 2026-07-22 22:20:23 +08:00
qwqcode 9fe237660c chore(scripts): bound registry downloads 2026-07-22 22:20:23 +08:00
qwqcode eff4a0b15d test: remove races from async tests 2026-07-22 22:20:23 +08:00
qwqcode 5b0e4c866e fix(api): validate vote status targets 2026-07-22 22:20:23 +08:00
qwqcode 617de2cdf1 fix(scripts): harden npm publish check 2026-07-22 22:20:23 +08:00
qwqcode 36c47104ca test(ui): cover sanitizer attribute allowlist 2026-07-22 22:13:45 +08:00
qwqcode 8460ae387f chore(deps): bump ci deps
Update checkout, setup, cache, artifact, Docker, CodeQL, Codecov, repository-dispatch, and supporting GitHub Actions to their current major releases.

Move the builder and runtime images to Go 1.26.5 on Alpine 3.24 while preserving workflow inputs and release artifact layout.
2026-07-22 22:13:44 +08:00
qwqcode e6a5c2d4b9 fix(scripts): import Buffer explicitly
Use the node:buffer entry point instead of relying on an implicit global when the plugin registry fetch script computes response sizes.
2026-07-22 22:13:44 +08:00
qwqcode 1cd9cdb445 test(ui): isolate browser storage from Node
Disable Node's experimental Web Storage shim for Vitest and coverage runs so jsdom consistently provides window.localStorage on supported Node 22 and 24 releases.
2026-07-22 22:13:44 +08:00
qwqcode 117908e57b fix(ui): replace vulnerable HTML sanitizer
Replace the unmaintained insane parser with DOMPurify to remove its known ReDoS exposure while retaining Artalk's tag, attribute, class, style, and URL scheme allowlists.

Add malicious markup, URL filtering, allowlist compatibility, and former ReDoS payload tests.
2026-07-22 22:13:44 +08:00
qwqcode b47eed5303 chore(deps): bump frontend deps
Update the UI, sidebar, plugins, documentation, lint, test, and build toolchains, including Vite 8.1, Marked 18.0.7, TypeScript 6, and current framework packages.

Pin patched transitive dependencies, move pnpm policy to the workspace configuration, approve the esbuild install script, and refresh generated router metadata.
2026-07-22 22:13:44 +08:00
qwqcode f7a2aeac9b refactor(auth): migrate JWT handling to v5
Use RegisteredClaims and ParseWithClaims, restrict accepted tokens to HS256, and keep existing claim names and signatures wire-compatible.

Add regression coverage for generated and legacy tokens, expiry validation, and algorithm rejection.
2026-07-22 22:13:44 +08:00
qwqcode 5ff42483aa chore(deps): bump backend deps
Update the Go dependency graph, including x/crypto, x/image, x/net, Slack, database drivers, Fiber integrations, and supporting modules.

Bump the CGO-free SQLite driver to v1.2.1 and prepare golang-jwt/jwt v5 for the dedicated authentication migration.
2026-07-22 22:13:44 +08:00
qwqcode 8de7157a91 feat(go): upgrade golang to v1.26.5 2026-07-22 22:13:43 +08:00
qwqcode 747b184fa4 build: remove cgo from SQLite releases 2026-07-22 22:13:43 +08:00
Ikko Eltociear Ashimine f875fc4023 docs: add Japanese README (#1145) 2026-06-24 17:30:17 +01:00
Henrik Pettersenandqwqcode cf06808170 feat(auth): add SSO token exchange endpoint (#1134)
* feat(auth): add SSO token exchange endpoint

Adds POST /api/v2/sso/exchange — accepts an access token from an
external OIDC IdP (verified by calling the IdP's /userinfo) and issues
an Artalk session JWT for the user identified by the email claim.

Use case: a surrounding application that already runs OIDC (e.g. an
Auth0 SPA) can drive Artalk sign-in without showing Artalk's own popup
login UI. The widget reads the resulting JWT from
localStorage["ArtalkUser"].token and treats the user as fully
authenticated for is_login/is_admin checks.

Implementation:
- One new handler at server/handler/auth_sso_exchange.go (~110 lines)
- Reuses dao.FindCreateUser + common.LoginGetUserToken; same JWT
  format as /user/access_token, so no client-side changes needed
- Validation by calling {issuer}/userinfo with Authorization: Bearer
  — OIDC-standard, no JWKS dep added
- Gated by auth.enabled && auth.sso.enabled (404 otherwise)
- Returns the same ResponseUserLogin shape as /auth/email/login

Config additions (conf/artalk.example.yml):
  auth:
    sso:
      enabled: false
      issuer: ""   # e.g. "tenant.auth0.com"

Route registered alongside the existing social-login routes in
server/server.go.

Verified by go build ./... (clean).

* chore(config): regenerate env-path cache with auth.sso entries

Required so koanf actually maps ATK_AUTH_SSO_ENABLED/ISSUER env vars
to the new auth.sso config block introduced in feat: add SSO token
exchange endpoint. Without this regen the env vars are silently
ignored and the endpoint stays in its disabled state.

* docs: add sso token exchange section for auth docs

---------

Co-authored-by: qwqcode <qwqcode@gmail.com>
2026-06-14 17:02:58 +01:00
xaoxuu 3b00a5e356 chore(docs): add xaoxuu to showcase (#1098) 2026-06-14 10:57:38 +01:00
Eser Şahinerandqwqcode 3d9c484a5d docs(i18n): add Turkish (tr-TR) language support (#1119)
* feat(i18n): add Turkish (tr-TR) language support

* chore: fix lint

---------

Co-authored-by: qwqcode <qwqcode@gmail.com>
2026-06-14 10:53:33 +01:00
Garnet dcac8cc883 chore(docs): revise French landing page translations (#1075) 2026-06-14 09:37:02 +01:00
Hsüan b39ec2a0a6 feat(db): add TLS support for MySQL with Google Cloud SQL (#1104)
* Support Google cloud SQL Mysql SSL

* fix typo
2026-05-05 21:55:54 +08:00
qwqcode 5c3df4776e feat(go): upgrade golang to v1.26.2 and some deps 2026-05-05 20:33:17 +08:00
Howie Xie 6ba19150df feat: add 'ugc' to link rel attributes (#1120) 2026-04-12 19:27:35 +08:00
Kegongteng efe62e93e2 chore(docs): Improve the transfer document (#1109) 2026-04-12 19:27:09 +08:00
她和她的猫 71a651153b chore(docs): correct docker arg name for skipping ui build (#1086) 2025-06-09 19:27:38 +08:00
ᡥᠠᡳᡤᡳᠶᠠ ᡥᠠᠯᠠ·ᠨᡝᡴᠣ 猫 b4c139bf9e fix(ui/ua): support OpenHarmony and more browsers (#1087) 2025-06-09 19:22:48 +08:00
198 changed files with 14399 additions and 7539 deletions

No files matched your search

+13
View File
@@ -1 +1,14 @@
node_modules
.git
.github
.DS_Store
.vscode
bin
docs/docs/*
!docs/docs/package.json
!docs/docs/tsconfig.json
local
test
ui/**/dist
**/playwright-report
**/test-results
+8 -2
View File
@@ -12,13 +12,19 @@ inputs:
runs:
using: "composite"
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
ISSUE_NUMBER: ${{ inputs.issue_number }}
STDOUT: ${{ inputs.content }}
with:
script: |
const issueNumber = Number.parseInt(process.env.ISSUE_NUMBER, 10)
if (!Number.isSafeInteger(issueNumber) || issueNumber <= 0) {
throw new Error('Invalid issue number')
}
github.rest.issues.createComment({
issue_number: ${{ inputs.issue_number }},
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: process.env.STDOUT
+8
View File
@@ -9,6 +9,8 @@ updates:
directory: /
schedule:
interval: monthly
cooldown:
default-days: 7
commit-message:
prefix: "chore(deps/go): "
groups:
@@ -23,6 +25,8 @@ updates:
directory: /
schedule:
interval: monthly
cooldown:
default-days: 7
commit-message:
prefix: "chore(deps/ui): "
groups:
@@ -37,6 +41,8 @@ updates:
directory: /
schedule:
interval: monthly
cooldown:
default-days: 7
commit-message:
prefix: "chore(deps/docker): "
groups:
@@ -49,6 +55,8 @@ updates:
directory: /.github/workflows
schedule:
interval: monthly
cooldown:
default-days: 7
commit-message:
prefix: "chore(deps/ci): "
groups:
+62 -42
View File
@@ -6,14 +6,15 @@ on:
version:
required: true
type: string
ref:
required: true
type: string
dry_run:
required: true
type: boolean
env:
GO_VERSION: 1.22.7
DOCKER_IMG: ghcr.io/goreleaser/goreleaser-cross
CACHE_DIR: /tmp/cache/docker-image
permissions:
contents: write
jobs:
build_publish:
@@ -21,64 +22,83 @@ jobs:
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.version }}
ref: ${{ inputs.ref }}
persist-credentials: false
# `go-releaser` will read the semver version from git tag name
# `git-chglog` will diff the tag and generate changelog
fetch-depth: 0
# 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: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
# Release inputs are verified from the lock files instead of shared caches.
cache: false
- name: Pull Docker Image
# if: steps.docker-cache.outputs.cache-hit != 'true'
run: |
docker pull "${DOCKER_IMG}:v${GO_VERSION}"
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.2
# 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 Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.x
package-manager-cache: false
- name: Setup git-chglog
run: |
curl -sL $(curl -s https://api.github.com/repos/git-chglog/git-chglog/releases/latest \
| grep -oP '"https://.+linux_amd64.tar.gz"' | tr -d \") | tar -C /usr/local/bin -xz git-chglog
git-chglog --version
GOBIN="$RUNNER_TEMP/bin" go install github.com/git-chglog/git-chglog/cmd/git-chglog@v0.15.4
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
go version -m "$RUNNER_TEMP/bin/git-chglog"
- name: Create local tag for dry run
if: ${{ inputs.dry_run }}
env:
VERSION: ${{ inputs.version }}
run: |
if git rev-parse --quiet --verify "refs/tags/$VERSION" > /dev/null; then
tag_commit="$(git rev-list -n 1 "$VERSION")"
head_commit="$(git rev-parse HEAD)"
if [[ "$tag_commit" != "$head_commit" ]]; then
echo "Tag $VERSION already points to $tag_commit, expected $head_commit."
exit 1
fi
else
git tag "$VERSION"
fi
- name: Pre Build
run: |-
mkdir -p local
# github token
echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" > local/.release-env
# changelog
git-chglog --config .github/chglog/config.yml ${{ inputs.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
env:
VERSION: ${{ inputs.version }}
- name: Print Relase Notes
- name: Print Release Notes
run: cat local/release-notes.md
- name: Validate release build
if: ${{ inputs.dry_run }}
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser
version: v2.17.0
args: release --clean --release-notes local/release-notes.md --skip=publish
- name: Build and Release
run: |
docker run \
--rm \
--privileged \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $(pwd)/sysroot:/sysroot \
-v $(pwd):/repo \
-w /repo \
--env-file local/.release-env \
ghcr.io/goreleaser/goreleaser-cross:v${GO_VERSION} \
release --release-notes local/release-notes.md ${{ inputs.dry_run && '--skip=publish' || '' }}
if: ${{ !inputs.dry_run }}
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
distribution: goreleaser
version: v2.17.0
args: release --clean --release-notes local/release-notes.md
env:
GITHUB_TOKEN: ${{ github.token }}
+250 -44
View File
@@ -6,77 +6,283 @@ on:
version:
required: true
type: string
ref:
required: true
type: string
commit:
required: true
type: string
dry_run:
required: true
type: boolean
target:
required: true
type: string
secrets:
DOCKERHUB_USERNAME:
required: false
DOCKERHUB_TOKEN:
required: false
permissions:
contents: read
env:
DIGEST_DIR: /tmp/digests
DOCKER_IMG: artalk/artalk-go
jobs:
build_publish:
runs-on: ubuntu-latest
ui:
name: Build Docker UI assets
runs-on: ubuntu-24.04
steps:
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Verify release source
env:
RELEASE_COMMIT: ${{ inputs.commit }}
run: |
if [[ ! "$RELEASE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid release commit: $RELEASE_COMMIT"
exit 1
fi
checkout_commit="$(git rev-parse HEAD)"
if [[ "$checkout_commit" != "$RELEASE_COMMIT" ]]; then
echo "Checked out $checkout_commit, expected $RELEASE_COMMIT."
exit 1
fi
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.2
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build UI
run: make build-frontend
- name: Upload UI assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: docker-ui-src
path: |
public
ui/artalk/dist
if-no-files-found: error
retention-days: 1
build:
name: Build Docker (${{ matrix.platform }})
needs: ui
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
platform: linux/amd64
platform_pair: linux_amd64
- runner: ubuntu-24.04-arm
platform: linux/arm64
platform_pair: linux_arm64
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.version }}
ref: ${{ inputs.ref }}
persist-credentials: false
# https://github.com/docker/metadata-action
- name: Gen docker meta
- name: Verify release source
env:
RELEASE_COMMIT: ${{ inputs.commit }}
run: |
if [[ ! "$RELEASE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid release commit: $RELEASE_COMMIT"
exit 1
fi
checkout_commit="$(git rev-parse HEAD)"
if [[ "$checkout_commit" != "$RELEASE_COMMIT" ]]; then
echo "Checked out $checkout_commit, expected $RELEASE_COMMIT."
exit 1
fi
- name: Download UI assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: docker-ui-src
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
context: git
images: |
${{ env.DOCKER_IMG }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
images: ${{ env.DOCKER_IMG }}
tags: type=raw,value=build-${{ matrix.platform_pair }}
# https://github.com/docker/login-action
- name: Login to DockerHub
uses: docker/login-action@v3
if: ${{ !inputs.dry_run }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# https://github.com/docker/setup-qemu-action
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: ${{ inputs.target }}
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
# Build UI outside of Docker to speed up cross-platform builds
- name: Build UI
run: |
make build-frontend
# https://github.com/docker/build-push-action
- name: Build and Push
id: build
uses: docker/build-push-action@v6
- name: Build dry-run image
if: ${{ inputs.dry_run }}
id: build_dry_run
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
push: ${{ !inputs.dry_run }}
context: .
file: ./Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }}
build-args: |
APP_VERSION=${{ inputs.version }}
APP_COMMIT_HASH=${{ inputs.commit }}
SKIP_UI_BUILD=true
platforms: ${{ inputs.target }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=docker
cache-from: type=gha,scope=release-${{ matrix.platform_pair }}
cache-to: type=gha,mode=max,scope=release-${{ matrix.platform_pair }}
- name: Print image digest
run: echo ${{ steps.build.outputs.digest }}
- name: Build and push image by digest
if: ${{ !inputs.dry_run }}
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ./Dockerfile
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_VERSION=${{ inputs.version }}
APP_COMMIT_HASH=${{ inputs.commit }}
SKIP_UI_BUILD=true
outputs: type=image,push=true,name=${{ env.DOCKER_IMG }},push-by-digest=true,name-canonical=true
cache-from: type=gha,scope=release-${{ matrix.platform_pair }}
cache-to: type=gha,mode=max,scope=release-${{ matrix.platform_pair }}
provenance: mode=max
sbom: true
- name: Smoke test dry-run image
if: ${{ inputs.dry_run }}
env:
IMAGE: ${{ steps.meta.outputs.tags }}
VERSION: ${{ inputs.version }}
run: |
output="$(docker run --rm --entrypoint /artalk "$IMAGE" version)"
grep --fixed-strings "Artalk ($VERSION/" <<< "$output"
- name: Smoke test published image
if: ${{ !inputs.dry_run }}
env:
IMAGE: ${{ env.DOCKER_IMG }}@${{ steps.build.outputs.digest }}
VERSION: ${{ inputs.version }}
run: |
docker pull "$IMAGE"
output="$(docker run --rm --entrypoint /artalk "$IMAGE" version)"
grep --fixed-strings "Artalk ($VERSION/" <<< "$output"
- name: Export digest
if: ${{ !inputs.dry_run }}
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
mkdir -p "$DIGEST_DIR"
touch "$DIGEST_DIR/${DIGEST#sha256:}"
- name: Upload digest
if: ${{ !inputs.dry_run }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-digest-${{ matrix.platform_pair }}
path: ${{ env.DIGEST_DIR }}/*
if-no-files-found: error
retention-days: 1
merge:
name: Publish Docker manifest
if: ${{ !inputs.dry_run }}
needs: build
runs-on: ubuntu-24.04
steps:
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ env.DIGEST_DIR }}
pattern: release-digest-*
merge-multiple: true
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Prepare release metadata
env:
RELEASE_COMMIT: ${{ inputs.commit }}
run: |
if [[ ! "$RELEASE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid release commit: $RELEASE_COMMIT"
exit 1
fi
echo "RELEASE_COMMIT_SHORT=${RELEASE_COMMIT:0:7}" >> "$GITHUB_ENV"
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.DOCKER_IMG }}
tags: |
type=semver,pattern={{version}},value=${{ inputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ inputs.version }}
type=raw,value=sha-${{ env.RELEASE_COMMIT_SHORT }}
- name: Login to DockerHub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push manifest
env:
METADATA: ${{ steps.meta.outputs.json }}
run: |
mapfile -t tags < <(jq -r '.tags[]' <<< "$METADATA")
tag_args=()
for tag in "${tags[@]}"; do
tag_args+=(--tag "$tag")
done
shopt -s nullglob
digest_files=("$DIGEST_DIR"/*)
if [[ "${#digest_files[@]}" -ne 2 ]]; then
echo "Expected 2 platform digests, found ${#digest_files[@]}."
exit 1
fi
sources=()
for digest_file in "${digest_files[@]}"; do
digest="$(basename "$digest_file")"
if [[ ! "$digest" =~ ^[0-9a-f]{64}$ ]]; then
echo "Invalid image digest: $digest"
exit 1
fi
sources+=("$DOCKER_IMG@sha256:$digest")
done
docker buildx imagetools create "${tag_args[@]}" "${sources[@]}"
- name: Inspect image
env:
IMAGE: ${{ env.DOCKER_IMG }}:${{ steps.meta.outputs.version }}
run: docker buildx imagetools inspect "$IMAGE"
+12 -3
View File
@@ -6,6 +6,9 @@ on:
version:
required: true
type: string
ref:
required: true
type: string
dry_run:
required: true
type: boolean
@@ -13,18 +16,24 @@ on:
required: true
type: number
permissions:
contents: read
issues: write
pull-requests: write
jobs:
done:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.version }}
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Comment done info
uses: ./.github/actions/comment
if: ${{ inputs.issue_number != 0 }}
if: ${{ !inputs.dry_run && inputs.issue_number != 0 }}
with:
issue_number: ${{ inputs.issue_number }}
content: |-
+216 -188
View File
@@ -1,5 +1,5 @@
name: Build Nightly
run-name: Build Nightly
run-name: Build Nightly ${{ inputs.dry_run && '(🧪 Dry-Run)' || '' }}
on:
schedule:
@@ -7,72 +7,64 @@ on:
workflow_dispatch:
inputs:
dry_run:
description: "Dry run"
description: "Build without publishing"
type: boolean
default: true
permissions:
contents: read
concurrency:
group: build-nightly
cancel-in-progress: false
env:
DIST_DIR: /tmp/builds
DIGEST_DIR: /tmp/digests
DOCKER_IMG: artalk/artalk-go
DOCKER_BUILD_ARGS: |-
SKIP_UI_BUILD=true
jobs:
# Prepare check should run
prepare:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
outputs:
skip: ${{ steps.check.outputs.skip }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check should run
- name: Check whether the scheduled build should run
id: check
if: github.event_name == 'schedule'
run: |
if [[ "$(git log --since='24 hours ago' | wc -l)" -eq 0 ]] || \
if [[ "$(git log --since='24 hours ago' --oneline | wc -l)" -eq 0 ]] || \
[[ "$GITHUB_REPOSITORY" != "ArtalkJS/Artalk" ]]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "skip=true" >> "$GITHUB_OUTPUT"
fi
# Bulid UI first
# (build ui outside of docker to speed up cross-platform builds)
ui:
name: Build Nightly UI assets
if: ${{ needs.prepare.outputs.skip != 'true' }}
needs: prepare
runs-on: macos-14
runs-on: ubuntu-24.04
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 9.10.0
version: 10.33.2
- name: Setup node
uses: actions/setup-node@v4
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
registry-url: https://registry.npmjs.org/
cache: "pnpm"
- name: Get pnpm store directory
shell: bash
run: |
echo "PNPM_STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.PNPM_STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
node-version: 22.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -81,273 +73,309 @@ jobs:
id: build_ui
run: |
make build-frontend
echo "ui_tarball=$(make -s pack-frontend)" >> $GITHUB_OUTPUT
echo "ui_tarball=$(make -s pack-frontend)" >> "$GITHUB_OUTPUT"
- name: Upload UI dist
uses: actions/upload-artifact@v4
- name: Upload UI assets
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ui-src
name: nightly-ui-src
path: |
public
ui/artalk/dist
if-no-files-found: error
retention-days: 1
- name: Upload UI tarball (included in release)
uses: actions/upload-artifact@v4
- name: Upload UI tarball
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-ui-tarball
path: ${{ steps.build_ui.outputs.ui_tarball }}
if-no-files-found: error
retention-days: 1
#
# Build app on different platforms
#
build:
name: Build Nightly (${{ matrix.platform }})
needs: ui
runs-on: ${{ matrix.os }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
# - macos-14
# :(
# native Apple m1 arm64 is not supported nested virtualization yet, so docker qemu is not working.
# https://github.com/actions/runner-images/issues/9460
# https://docs.veertu.com/anka/anka-virtualization-cli/nested-virtualization/
#
# wait for arm64 linux runner support on github actions.
platform:
- linux/amd64
- linux/arm64
# exclude:
# # exclude build x86 on arm, and vice versa.
# - { os: macos-14, platform: linux/amd64 }
# - { os: ubuntu-latest, platform: linux/arm64 }
include:
- runner: ubuntu-24.04
platform: linux/amd64
platform_pair: linux_amd64
- runner: ubuntu-24.04-arm
platform: linux/arm64
platform_pair: linux_arm64
steps:
- name: Prepare
run: |
PF="${{ matrix.platform }}"
echo "PLATFORM_PAIR=${PF//\//_}" >> $GITHUB_ENV
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Setup docker on macos
if: startsWith(matrix.os, 'macos')
uses: crazy-max/ghaction-setup-docker@v3
- name: Download UI assets
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: nightly-ui-src
- name: Gen docker meta
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.DOCKER_IMG }}
tags: type=raw,value=nightly
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
tags: type=raw,value=nightly-build-${{ matrix.platform_pair }}
- name: Login to DockerHub
uses: docker/login-action@v3
if: ${{ github.event_name == 'schedule' || !inputs.dry_run }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Download UI before build
uses: actions/download-artifact@v4
with:
name: ui-src
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Build and push by digest
uses: docker/build-push-action@v6
id: build
- name: Build dry-run image
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
build-args: ${{ env.DOCKER_BUILD_ARGS }}
# The `type=image,push=true` must the last one,
# because the `build.outputs.digest` will output the last image's digest.
# The digests will be used in the merge step.
build-args: |
APP_COMMIT_HASH=${{ github.sha }}
SKIP_UI_BUILD=true
outputs: type=docker,name=${{ steps.meta.outputs.tags }}
cache-from: type=gha,scope=nightly-${{ matrix.platform_pair }}
cache-to: type=gha,mode=max,scope=nightly-${{ matrix.platform_pair }}
- name: Build and push image by digest
if: ${{ github.event_name == 'schedule' || !inputs.dry_run }}
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_COMMIT_HASH=${{ github.sha }}
SKIP_UI_BUILD=true
outputs: |
type=docker,name=${{ env.DOCKER_IMG }}
type=docker,name=${{ steps.meta.outputs.tags }}
type=image,push=true,name=${{ env.DOCKER_IMG }},push-by-digest=true,name-canonical=true
cache-from: type=gha,scope=nightly-${{ matrix.platform_pair }}
cache-to: type=gha,mode=max,scope=nightly-${{ matrix.platform_pair }}
- name: Smoke test image
env:
IMAGE: ${{ steps.meta.outputs.tags }}
run: docker run --rm --entrypoint /artalk "$IMAGE" version
- name: Export digest
if: ${{ github.event_name == 'schedule' || !inputs.dry_run }}
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
mkdir -p $DIGEST_DIR
digest="${{ steps.build.outputs.digest }}"
touch "$DIGEST_DIR/${digest#sha256:}"
mkdir -p "$DIGEST_DIR"
touch "$DIGEST_DIR/${DIGEST#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
if: ${{ github.event_name == 'schedule' || !inputs.dry_run }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: digests-${{ env.PLATFORM_PAIR }}
name: nightly-digest-${{ matrix.platform_pair }}
path: ${{ env.DIGEST_DIR }}/*
if-no-files-found: error
retention-days: 1
# Export docker image
- name: Export Docker Image
- name: Export Docker image
id: docker
env:
IMAGE: ${{ steps.meta.outputs.tags }}
PLATFORM_PAIR: ${{ matrix.platform_pair }}
run: |
IMG_FILE="artalk_docker_nigthly_$(date +'%Y%m%d')_${PLATFORM_PAIR}.tar.gz"
docker save ${{ env.DOCKER_IMG }} | gzip > $IMG_FILE
echo "img_file=$IMG_FILE" >> $GITHUB_OUTPUT
image_file="artalk_docker_nigthly_$(date +'%Y%m%d')_${PLATFORM_PAIR}.tar.gz"
docker save "$IMAGE" | gzip > "$image_file"
echo "image_file=$image_file" >> "$GITHUB_OUTPUT"
# App
- name: Pack app
id: app
env:
IMAGE: ${{ steps.meta.outputs.tags }}
PLATFORM_PAIR: ${{ matrix.platform_pair }}
run: |
BASE_DIR="/tmp/app_pkg"
NAME="artalk_nigthly_$(date +'%Y%m%d')_${PLATFORM_PAIR}"
SUB_DIR="$BASE_DIR/$NAME"
PKG_FILE="$BASE_DIR/$NAME.tar.gz"
base_dir="/tmp/app_pkg"
name="artalk_nigthly_$(date +'%Y%m%d')_${PLATFORM_PAIR}"
package_dir="$base_dir/$name"
package_file="$base_dir/$name.tar.gz"
# bin file
mkdir -p $SUB_DIR
docker run --rm --entrypoint cat ${{ env.DOCKER_IMG }} /artalk > "$SUB_DIR/artalk"
chmod +x "$SUB_DIR/artalk"
mkdir -p "$package_dir"
docker run --rm --entrypoint cat "$IMAGE" /artalk > "$package_dir/artalk"
chmod +x "$package_dir/artalk"
# doc file
cp conf/artalk.example.yml "$SUB_DIR/artalk.yml"
cp README.md LICENSE CHANGELOG.md "$SUB_DIR"
cp conf/artalk.example.yml "$package_dir/artalk.yml"
cp README.md LICENSE CHANGELOG.md "$package_dir"
# create package file
tar -czf $PKG_FILE -C $BASE_DIR $NAME
echo "app_pkg_file=$PKG_FILE" >> $GITHUB_OUTPUT
tar -czf "$package_file" -C "$base_dir" "$name"
echo "package_file=$package_file" >> "$GITHUB_OUTPUT"
# Move build artifacts
- name: Move build artifacts
- name: Collect build artifacts
env:
APP_PACKAGE: ${{ steps.app.outputs.package_file }}
DOCKER_IMAGE: ${{ steps.docker.outputs.image_file }}
run: |
mkdir -p $DIST_DIR
mv ${{ steps.app.outputs.app_pkg_file }} $DIST_DIR
mv ${{ steps.docker.outputs.img_file }} $DIST_DIR
mkdir -p "$DIST_DIR"
mv "$APP_PACKAGE" "$DIST_DIR"
mv "$DOCKER_IMAGE" "$DIST_DIR"
# Upload build artifacts
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build-${{ env.PLATFORM_PAIR }}
name: build-${{ matrix.platform_pair }}
path: ${{ env.DIST_DIR }}/*
if-no-files-found: error
retention-days: 1
# Merge docker digests from different platforms
# Reference: https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
docker_merge:
runs-on: ubuntu-latest
name: Publish Nightly Docker manifest
if: ${{ github.event_name == 'schedule' || !inputs.dry_run }}
needs: build
runs-on: ubuntu-24.04
steps:
- name: Download digests
uses: actions/download-artifact@v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ env.DIGEST_DIR }}
pattern: digests-*
pattern: nightly-digest-*
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
- name: Docker meta
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ${{ env.DOCKER_IMG }}
tags: type=raw,value=nightly
- name: Login to Docker Hub
uses: docker/login-action@v3
- name: Login to DockerHub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create manifest list and push
working-directory: ${{ env.DIGEST_DIR }}
- name: Create and push manifest
env:
METADATA: ${{ steps.meta.outputs.json }}
run: |
TAGS_ARG=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
MINIFEST_SRCS=$(printf '${{ env.DOCKER_IMG }}@sha256:%s ' *)
mapfile -t tags < <(jq -r '.tags[]' <<< "$METADATA")
tag_args=()
for tag in "${tags[@]}"; do
tag_args+=(--tag "$tag")
done
docker buildx imagetools create ${TAGS_ARG} ${MINIFEST_SRCS}
shopt -s nullglob
digest_files=("$DIGEST_DIR"/*)
if [[ "${#digest_files[@]}" -ne 2 ]]; then
echo "Expected 2 platform digests, found ${#digest_files[@]}."
exit 1
fi
sources=()
for digest_file in "${digest_files[@]}"; do
digest="$(basename "$digest_file")"
if [[ ! "$digest" =~ ^[0-9a-f]{64}$ ]]; then
echo "Invalid image digest: $digest"
exit 1
fi
sources+=("$DOCKER_IMG@sha256:$digest")
done
docker buildx imagetools create "${tag_args[@]}" "${sources[@]}"
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.DOCKER_IMG }}:${{ steps.meta.outputs.version }}
env:
IMAGE: ${{ env.DOCKER_IMG }}:${{ steps.meta.outputs.version }}
run: docker buildx imagetools inspect "$IMAGE"
# Release
release:
runs-on: ubuntu-latest
needs: build
name: Publish Nightly GitHub release
if: ${{ github.event_name == 'schedule' || !inputs.dry_run }}
needs: [build, docker_merge]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Download builds
uses: actions/download-artifact@v4
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: ${{ env.DIST_DIR }}
pattern: build-*
merge-multiple: true
# Generate CHANGELOG
- name: Generate Changelog
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false
- name: Setup git-chglog
run: |
# install git-chglog
curl -sL $(curl -s https://api.github.com/repos/git-chglog/git-chglog/releases/latest \
| grep -oP '"https://.+linux_amd64.tar.gz"' | tr -d \") | tar -C /usr/local/bin -xz git-chglog
GOBIN="$RUNNER_TEMP/bin" go install github.com/git-chglog/git-chglog/cmd/git-chglog@v0.15.4
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
go version -m "$RUNNER_TEMP/bin/git-chglog"
CHANGELOG=$(git-chglog --config .github/chglog/config.yml --next-tag nightly nightly)
MD_FILE="/tmp/changelog.md"
echo -e "$CHANGELOG" > $MD_FILE
echo -e "\n> [!NOTE]\n" \
"> The Nightly version, automatically built based on the latest code, " \
"is not yet released and may be unstable. Please use it with caution. " \
"Docker user can run \`docker pull artalk/artalk-go:nightly\` to get " \
"the nightly build." >> $MD_FILE
# checksums.txt
- name: Calculate checksums.txt
- name: Generate changelog
run: |
cd $DIST_DIR
sha256sum * > checksums.txt
git-chglog --config .github/chglog/config.yml --next-tag nightly nightly > /tmp/changelog.md
cat >> /tmp/changelog.md <<'EOF'
# print checksums.txt
> [!NOTE]
> The Nightly version is automatically built from the latest code and may be unstable.
> Docker users can run `docker pull artalk/artalk-go:nightly` to get this build.
EOF
- name: Calculate checksums
run: |
cd "$DIST_DIR"
sha256sum -- * > checksums.txt
cat checksums.txt
FILES=$(find ${DIST_DIR} -type f -exec readlink -f {} \;)
echo -e "RELEASE_FILES<<EOF" >> $GITHUB_ENV
echo -e "$FILES" >> $GITHUB_ENV
echo -e "EOF" >> $GITHUB_ENV
- name: Create tag and push
- name: Move Nightly tag
env:
GH_TOKEN: ${{ github.token }}
run: |
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/nightly" > /dev/null 2>&1; then
gh api --method PATCH "repos/$GITHUB_REPOSITORY/git/refs/tags/nightly" \
-f sha="$GITHUB_SHA" -F force=true > /dev/null
else
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref=refs/tags/nightly -f sha="$GITHUB_SHA" > /dev/null
fi
tag_sha="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/nightly" --jq '.object.sha')"
if [[ "$tag_sha" != "$GITHUB_SHA" ]]; then
echo "Nightly tag points to $tag_sha, expected $GITHUB_SHA."
exit 1
fi
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release delete nightly --yes || true
git push origin :refs/tags/nightly || true
git tag -f nightly
git push -f origin nightly
- name: Release
uses: softprops/action-gh-release@v2
with:
prerelease: true
tag_name: nightly
name: 🧪 Nightly Version
body_path: /tmp/changelog.md
files: ${{ env.RELEASE_FILES }}
draft: false
mapfile -d '' release_files < <(find "$DIST_DIR" -type f -print0)
gh release create nightly "${release_files[@]}" \
--title "🧪 Nightly Version" \
--notes-file /tmp/changelog.md \
--prerelease \
--target "$GITHUB_SHA"
+223 -19
View File
@@ -9,53 +9,257 @@ on:
issue_number:
required: true
type: number
version:
required: false
type: string
default: ""
release_commit:
description: "Immutable commit to tag when the release tag does not exist"
required: false
type: string
default: ""
outputs:
version:
description: "Version"
value: ${{ jobs.tagging.outputs.version }}
ref:
description: "Git ref to build"
value: ${{ jobs.tagging.outputs.ref }}
commit:
description: "Release source commit"
value: ${{ jobs.tagging.outputs.commit }}
permissions:
contents: write
issues: write
pull-requests: write
jobs:
tagging:
runs-on: ubuntu-latest
steps:
- name: Get release version
id: version
env:
REF: ${{ github.head_ref || github.ref }}
REQUESTED_VERSION: ${{ inputs.version }}
run: |
# `github.head_ref` only exists for pull_request events
REF="${{ github.head_ref || github.ref }}"
VERSION="${REF#refs/tags/}"
VERSION="${VERSION#refs/heads/}"
VERSION="${VERSION#release/}"
echo "Version: $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
if [[ -n "$REQUESTED_VERSION" ]]; then
VERSION="$REQUESTED_VERSION"
else
VERSION="${REF#refs/tags/}"
VERSION="${VERSION#refs/heads/}"
VERSION="${VERSION#release/}"
fi
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version format: $VERSION, please check git ref or tag."
exit 1
fi
echo "Version: $VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Resolve release target
id: target
env:
DEFAULT_COMMIT: ${{ github.sha }}
DRY_RUN: ${{ inputs.dry_run }}
GH_TOKEN: ${{ github.token }}
REQUESTED_COMMIT: ${{ inputs.release_commit }}
VERSION: ${{ steps.version.outputs.version }}
run: |
resolve_tag_commit() {
local object_type="$1"
local object_sha="$2"
local depth=0
while [[ "$object_type" == "tag" ]]; do
depth=$((depth + 1))
if (( depth > 8 )); then
echo "Tag $VERSION has too many nested tag objects." >&2
return 1
fi
IFS=$'\t' read -r object_type object_sha < <(
gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" \
--jq '.object | [.type, .sha] | @tsv'
)
done
if [[ "$object_type" != "commit" ]]; then
echo "Tag $VERSION resolves to unsupported object type $object_type." >&2
return 1
fi
printf '%s\n' "$object_sha"
}
requested_commit="$(tr '[:upper:]' '[:lower:]' <<< "$REQUESTED_COMMIT")"
if [[ -n "$requested_commit" && ! "$requested_commit" =~ ^[0-9a-f]{40}$ ]]; then
echo "release_commit must be a full 40-character commit SHA."
exit 1
fi
tag_ref="$(
gh api "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/$VERSION" \
--jq ".[] | select(.ref == \"refs/tags/$VERSION\") | [.object.type, .object.sha] | @tsv"
)"
tag_exists=false
if [[ -n "$tag_ref" ]]; then
IFS=$'\t' read -r tag_type tag_sha <<< "$tag_ref"
release_commit="$(resolve_tag_commit "$tag_type" "$tag_sha")"
tag_exists=true
if [[ -n "$requested_commit" && "$release_commit" != "$requested_commit" ]]; then
echo "Tag $VERSION points to $release_commit, not requested commit $requested_commit."
exit 1
fi
elif [[ -n "$requested_commit" ]]; then
release_commit="$requested_commit"
elif [[ "$DRY_RUN" == "true" ]]; then
release_commit="$DEFAULT_COMMIT"
else
echo "Tag $VERSION does not exist and release_commit was not provided."
echo "Use the reviewed release pull request merge commit for a production retry."
exit 1
fi
verified_commit="$(
gh api "repos/$GITHUB_REPOSITORY/git/commits/$release_commit" --jq '.sha'
)"
if [[ "$verified_commit" != "$release_commit" ]]; then
echo "Unable to verify release commit $release_commit."
exit 1
fi
echo "commit=$release_commit" >> "$GITHUB_OUTPUT"
echo "tag_exists=$tag_exists" >> "$GITHUB_OUTPUT"
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.target.outputs.commit }}
fetch-depth: 0
persist-credentials: false
- name: Validate release source
env:
RELEASE_COMMIT: ${{ steps.target.outputs.commit }}
VERSION: ${{ steps.version.outputs.version }}
run: |
checkout_commit="$(git rev-parse HEAD)"
if [[ "$checkout_commit" != "$RELEASE_COMMIT" ]]; then
echo "Checked out $checkout_commit, expected $RELEASE_COMMIT."
exit 1
fi
git fetch --no-tags origin \
"+refs/heads/master:refs/remotes/origin/master"
if ! git merge-base --is-ancestor "$RELEASE_COMMIT" origin/master; then
echo "Release commit $RELEASE_COMMIT is not reachable from master."
exit 1
fi
package_version="${VERSION#v}"
app_version="$(
sed -n 's/^var Version = "\(v[0-9][0-9.]*\)"$/\1/p' internal/config/version.go
)"
ui_version="$(jq -r '.version' ui/artalk/package.json)"
docs_version="$(jq -r '.latest' docs/docs/code/ArtalkVersion.json)"
if [[ "$app_version" != "$VERSION" ||
"$ui_version" != "$package_version" ||
"$docs_version" != "$package_version" ]]; then
echo "Release commit versions do not match $VERSION: app=$app_version ui=$ui_version docs=$docs_version."
exit 1
fi
- name: Create Tag
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ github.token }}
RELEASE_COMMIT: ${{ steps.target.outputs.commit }}
TAG_EXISTS: ${{ steps.target.outputs.tag_exists }}
VERSION: ${{ steps.version.outputs.version }}
run: |
if [ ! $(git tag -l "${VERSION}") ]; then
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git tag -a $VERSION -m "Release $VERSION"
git push origin $VERSION
else
echo "Skip creating tag $VERSION, already exists."
resolve_tag_commit() {
local object_type="$1"
local object_sha="$2"
local depth=0
while [[ "$object_type" == "tag" ]]; do
depth=$((depth + 1))
if (( depth > 8 )); then
echo "Tag $VERSION has too many nested tag objects." >&2
return 1
fi
IFS=$'\t' read -r object_type object_sha < <(
gh api "repos/$GITHUB_REPOSITORY/git/tags/$object_sha" \
--jq '.object | [.type, .sha] | @tsv'
)
done
if [[ "$object_type" != "commit" ]]; then
echo "Tag $VERSION resolves to unsupported object type $object_type." >&2
return 1
fi
printf '%s\n' "$object_sha"
}
get_tag_ref() {
gh api "repos/$GITHUB_REPOSITORY/git/matching-refs/tags/$VERSION" \
--jq ".[] | select(.ref == \"refs/tags/$VERSION\") | [.object.type, .object.sha] | @tsv"
}
if [[ "$TAG_EXISTS" != "true" ]]; then
tag_object_sha="$(
gh api --method POST "repos/$GITHUB_REPOSITORY/git/tags" \
-f tag="$VERSION" \
-f message="Release $VERSION" \
-f object="$RELEASE_COMMIT" \
-f type=commit \
--jq '.sha'
)"
if ! create_error="$(
gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref="refs/tags/$VERSION" \
-f sha="$tag_object_sha" 2>&1
)"; then
tag_ref="$(get_tag_ref)"
if [[ -z "$tag_ref" ]]; then
printf '%s\n' "$create_error" >&2
echo "GitHub rejected tag $VERSION. Check the active tag rulesets; tag targets must not require linear history." >&2
exit 1
fi
echo "Tag $VERSION was created concurrently; verifying it."
else
echo "Created annotated tag $VERSION at $RELEASE_COMMIT."
fi
fi
tag_ref="$(get_tag_ref)"
if [[ -z "$tag_ref" ]]; then
echo "Tag $VERSION is missing after creation."
exit 1
fi
IFS=$'\t' read -r tag_type tag_sha <<< "$tag_ref"
tag_commit="$(resolve_tag_commit "$tag_type" "$tag_sha")"
if [[ "$tag_commit" != "$RELEASE_COMMIT" ]]; then
echo "Tag $VERSION points to $tag_commit, expected $RELEASE_COMMIT."
exit 1
fi
echo "Verified tag $VERSION at $RELEASE_COMMIT."
- name: Comment tag info to PR
uses: ./.github/actions/comment
if: ${{ inputs.issue_number != 0 }}
if: ${{ !inputs.dry_run && inputs.issue_number != 0 }}
with:
issue_number: ${{ inputs.issue_number }}
content: |-
🔖 Tagged version [${{ env.VERSION }}](https://github.com/ArtalkJS/Artalk/tree/${{ env.VERSION }}) and updated the [CHANGELOG.md](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md).
🔖 Tagged version [${{ steps.version.outputs.version }}](https://github.com/ArtalkJS/Artalk/tree/${{ steps.version.outputs.version }}) and updated the [CHANGELOG.md](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md).
outputs:
version: ${{ env.VERSION }}
version: ${{ steps.version.outputs.version }}
ref: ${{ inputs.dry_run && steps.target.outputs.commit || steps.version.outputs.version }}
commit: ${{ steps.target.outputs.commit }}
+41 -12
View File
@@ -6,31 +6,38 @@ on:
version:
required: true
type: string
ref:
required: true
type: string
dry_run:
required: true
type: boolean
permissions:
contents: read
id-token: write
jobs:
build_publish:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.version }}
ref: ${{ inputs.ref }}
persist-credentials: false
- name: Use pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 9.10.0
version: 10.33.2
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
registry-url: https://registry.npmjs.org/
cache: "pnpm"
node-version: 24.x
package-manager-cache: false
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -38,15 +45,37 @@ jobs:
- name: Build
run: pnpm build
- name: Publish
run: pnpm publish -F artalk --no-git-checks ${{ inputs.dry_run && '--dry-run' || '' }}
- name: Pack npm package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
package_version="v$(node --print "require('./ui/artalk/package.json').version")"
if [[ "$package_version" != "$VERSION" ]]; then
echo "Package version $package_version does not match release version $VERSION."
exit 1
fi
pnpm --filter artalk pack --pack-destination "$RUNNER_TEMP"
package_file="$RUNNER_TEMP/artalk-${package_version#v}.tgz"
if [[ ! -f "$package_file" ]]; then
echo "Expected package tarball was not created: $package_file"
exit 1
fi
echo "PACKAGE_FILE=$package_file" >> "$GITHUB_ENV"
- name: Validate package
if: ${{ inputs.dry_run }}
run: npm publish "$PACKAGE_FILE" --dry-run --access public
- name: Publish
if: ${{ !inputs.dry_run }}
# npm validates build.yml, the calling workflow, for trusted publishing.
run: npm publish "$PACKAGE_FILE" --access public
# GitHub Packages
#
# Setup .npmrc file to publish to GitHub Packages
# - uses: actions/setup-node@v4
# - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
# with:
# registry-url: 'https://npm.pkg.github.com'
# # Publish to GitHub Packages
+61 -19
View File
@@ -1,5 +1,5 @@
name: Build and Publish
run-name: Build and Publish (${{ github.head_ref || github.ref }}) ${{ inputs.dry_run && '(🧪 Dry-Run)' || '' }}
run-name: Build and Publish (${{ inputs.version || github.head_ref || github.ref }}) ${{ inputs.dry_run && '(🧪 Dry-Run)' || '' }}
on:
pull_request:
@@ -12,11 +12,34 @@ on:
description: "Dry run"
type: boolean
default: true
version:
description: "Release version (for example, v2.10.0)"
type: string
required: true
release_commit:
description: "Full release commit SHA (required when a production retry has no tag)"
type: string
required: false
build_items:
description: "Build items"
type: string
type: choice
options:
- "ui,app,docker"
- "ui,app"
- "ui,docker"
- "app,docker"
- ui
- app
- docker
default: "ui,app,docker"
permissions:
contents: read
concurrency:
group: release-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: false
jobs:
# Tag the release version code before building
# Getting the version code from the branch name or git ref.
@@ -26,57 +49,76 @@ jobs:
# (which the branch name start with `relase/`)
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && startsWith(github.head_ref, 'release/'))
(github.event.pull_request.merged == true &&
github.event.pull_request.head.repo.full_name == github.repository &&
startsWith(github.head_ref, 'release/'))
uses: ./.github/workflows/build-tagging.yml
permissions:
contents: write
issues: write
pull-requests: write
with:
issue_number: ${{ github.event.pull_request.number || 0 }}
dry_run: ${{ inputs.dry_run || false }}
secrets: inherit
version: ${{ inputs.version || '' }}
release_commit: ${{ github.event.pull_request.merge_commit_sha || inputs.release_commit || '' }}
# Build the UI
ui:
if: inputs.build_items == '' || contains(inputs.build_items, 'ui')
if: inputs.build_items == '' || contains(format(',{0},', inputs.build_items), ',ui,')
needs: tagging
uses: ./.github/workflows/build-ui.yml
with:
version: ${{ needs.tagging.outputs.version }}
ref: ${{ needs.tagging.outputs.ref }}
dry_run: ${{ inputs.dry_run || false }}
secrets: inherit
permissions:
contents: read
id-token: write
# Build the Docker image
docker:
if: inputs.build_items == '' || contains(inputs.build_items, 'docker')
if: inputs.build_items == '' || contains(format(',{0},', inputs.build_items), ',docker,')
needs: tagging
uses: ./.github/workflows/build-docker.yml
# Parallel build the multi-arch docker image
strategy:
matrix:
# since docker push actions will override the previous images,
# so need to build `linux/amd64` and push first,
# then build `linux/amd64` twice with other archs (build very slow so keep it last).
target: ["linux/amd64", "linux/amd64,linux/arm64"]
with:
version: ${{ needs.tagging.outputs.version }}
ref: ${{ needs.tagging.outputs.ref }}
commit: ${{ needs.tagging.outputs.commit }}
dry_run: ${{ inputs.dry_run || false }}
target: ${{ matrix.target }}
secrets: inherit
permissions:
contents: read
secrets:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
# Build the binary app
app:
if: inputs.build_items == '' || contains(inputs.build_items, 'app')
if: inputs.build_items == '' || contains(format(',{0},', inputs.build_items), ',app,')
needs: tagging
uses: ./.github/workflows/build-app.yml
with:
version: ${{ needs.tagging.outputs.version }}
ref: ${{ needs.tagging.outputs.ref }}
dry_run: ${{ inputs.dry_run || false }}
secrets: inherit
permissions:
contents: write
# All builds are complete
done:
needs: [tagging, ui, docker, app]
if: |
always() &&
needs.tagging.result == 'success' &&
!contains(needs.*.result, 'failure') &&
!contains(needs.*.result, 'cancelled')
uses: ./.github/workflows/build-done.yml
permissions:
contents: read
issues: write
pull-requests: write
with:
version: ${{ needs.tagging.outputs.version }}
ref: ${{ needs.tagging.outputs.ref }}
issue_number: ${{ github.event.pull_request.number || 0 }}
dry_run: ${{ inputs.dry_run || false }}
secrets: inherit
+13 -4
View File
@@ -22,6 +22,13 @@ on:
schedule:
- cron: "40 17 * * 4"
permissions:
contents: read
concurrency:
group: codeql-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
analyze:
name: Analyze
@@ -42,11 +49,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -59,7 +68,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -72,6 +81,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
with:
category: "/language:${{matrix.language}}"
+16 -7
View File
@@ -5,33 +5,42 @@ on:
branches: ["master"]
paths: ["docs/**"]
permissions:
contents: read
concurrency:
group: deploy-docs-cn
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Use pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 9.10.0
version: 10.33.2
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile
- name: Build Docs
run: pnpm build:docs
- name: Deploy
uses: TencentCloud/cos-action@v1
uses: TencentCloud/cos-action@5ee3807d5ff6fddadbbb1d3e658c0690863de8ec # v1
with:
secret_id: ${{ secrets.TENCENT_CLOUD_SECRET_ID }}
secret_key: ${{ secrets.TENCENT_CLOUD_SECRET_KEY }}
+178 -26
View File
@@ -21,67 +21,219 @@ on:
type: boolean
default: false
permissions:
contents: read
concurrency:
group: prepare-release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: master
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20.x
registry-url: https://registry.npmjs.org/
- name: Setup semver
run: npm install -g semver
persist-credentials: false
- name: Handle Version Number
env:
CUSTOM_VERSION: ${{ inputs.custom_version }}
GH_TOKEN: ${{ github.token }}
SEMVER_INCREMENT: ${{ inputs.semver }}
run: |
PREV_VERSION="$(git describe --tags --abbrev=0 --match 'v*')"
if [ -n "${{ inputs.custom_version }}" ]; then
NEXT_VERSION="${{ inputs.custom_version }}"
else
NEXT_VERSION="v$(semver --increment ${{ inputs.semver }} ${PREV_VERSION})"
if [[ ! "$PREV_VERSION" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
echo "Invalid previous version: $PREV_VERSION"
exit 1
fi
echo "PREV_VERSION=${PREV_VERSION}" >> $GITHUB_ENV
echo "VERSION=${NEXT_VERSION}" >> $GITHUB_ENV
if ! git merge-base --is-ancestor "$PREV_VERSION" HEAD; then
echo "Latest release tag $PREV_VERSION is not an ancestor of master."
exit 1
fi
CURRENT_APP_VERSION="$(
sed -n 's/^var Version = "\(v[0-9][0-9.]*\)"$/\1/p' internal/config/version.go
)"
CURRENT_UI_VERSION="$(jq -r '.version' ui/artalk/package.json)"
CURRENT_DOCS_VERSION="$(jq -r '.latest' docs/docs/code/ArtalkVersion.json)"
CURRENT_PACKAGE_VERSION="${CURRENT_APP_VERSION#v}"
if [[ -z "$CURRENT_APP_VERSION" ||
"$CURRENT_UI_VERSION" != "$CURRENT_PACKAGE_VERSION" ||
"$CURRENT_DOCS_VERSION" != "$CURRENT_PACKAGE_VERSION" ]]; then
echo "Release versions are inconsistent: app=$CURRENT_APP_VERSION ui=$CURRENT_UI_VERSION docs=$CURRENT_DOCS_VERSION."
exit 1
fi
if [[ "$CURRENT_APP_VERSION" != "$PREV_VERSION" ]]; then
echo "Source version $CURRENT_APP_VERSION does not match the latest release tag $PREV_VERSION."
echo "If the release PR was already merged, recover it with Build and Publish instead of preparing the release again."
exit 1
fi
if [[ -n "$CUSTOM_VERSION" ]]; then
NEXT_VERSION="$CUSTOM_VERSION"
else
major="${BASH_REMATCH[1]}"
minor="${BASH_REMATCH[2]}"
patch="${BASH_REMATCH[3]}"
case "$SEMVER_INCREMENT" in
major) NEXT_VERSION="v$((major + 1)).0.0" ;;
minor) NEXT_VERSION="v${major}.$((minor + 1)).0" ;;
patch) NEXT_VERSION="v${major}.${minor}.$((patch + 1))" ;;
*) echo "Invalid version increment: $SEMVER_INCREMENT"; exit 1 ;;
esac
fi
if [[ ! "$NEXT_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Invalid version: $NEXT_VERSION"
exit 1
fi
version_is_greater() {
local next_version="${1#v}"
local previous_version="${2#v}"
local -a next_parts previous_parts
local index next_part previous_part
IFS='.' read -r -a next_parts <<< "$next_version"
IFS='.' read -r -a previous_parts <<< "$previous_version"
for index in 0 1 2; do
next_part=$((10#${next_parts[$index]}))
previous_part=$((10#${previous_parts[$index]}))
if (( next_part > previous_part )); then
return 0
fi
if (( next_part < previous_part )); then
return 1
fi
done
return 1
}
if ! version_is_greater "$NEXT_VERSION" "$PREV_VERSION"; then
echo "Release version $NEXT_VERSION must be greater than $PREV_VERSION."
exit 1
fi
if git show-ref --verify --quiet "refs/tags/$NEXT_VERSION"; then
echo "Release tag $NEXT_VERSION already exists."
exit 1
fi
release_branch="release/$NEXT_VERSION"
existing_pr="$(
gh pr list \
--repo "$GITHUB_REPOSITORY" \
--head "$release_branch" \
--base master \
--state open \
--json number,url \
--jq 'if length == 0 then "" else "#\(.[0].number) \(.[0].url)" end'
)"
if [[ -n "$existing_pr" ]]; then
echo "Release pull request $existing_pr already exists for $NEXT_VERSION."
exit 1
fi
echo "PREV_VERSION=${PREV_VERSION}" >> "$GITHUB_ENV"
echo "VERSION=${NEXT_VERSION}" >> "$GITHUB_ENV"
- name: Print Next Version
run: echo "${VERSION}"
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false
- name: Setup git-chglog
run: |
curl -sL $(curl -s https://api.github.com/repos/git-chglog/git-chglog/releases/latest \
| grep -oP '"https://.+linux_amd64.tar.gz"' | tr -d \") | tar -C /usr/local/bin -xz git-chglog
git-chglog --version
GOBIN="$RUNNER_TEMP/bin" go install github.com/git-chglog/git-chglog/cmd/git-chglog@v0.15.4
echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH"
go version -m "$RUNNER_TEMP/bin/git-chglog"
- name: Version Changelog
run: |
# prepend changelog
changelog=$(git-chglog --config .github/chglog/config.yml --next-tag "${VERSION}" "${VERSION}")
echo -e "${changelog}\n" | cat - CHANGELOG.md > temp && mv temp CHANGELOG.md
changelog="$(git-chglog --config .github/chglog/config.yml --next-tag "$VERSION" "$VERSION")"
temp_file="$(mktemp CHANGELOG.md.XXXXXX)"
printf '%s\n\n' "$changelog" | cat - CHANGELOG.md > "$temp_file"
mv "$temp_file" CHANGELOG.md
# modify the version for `internal/config/version.go`
perl -i -pe 's/const Version = ".*?"/const Version = "'${VERSION}'"/g' internal/config/version.go
perl -i -pe 's/var Version = ".*?"/var Version = "$ENV{VERSION}"/g' internal/config/version.go
# modify the version for `package.json`
perl -i -pe 's/"version": ".*?"/"version": "'${VERSION#v}'"/g' ui/artalk/package.json
PACKAGE_VERSION="${VERSION#v}"
PACKAGE_VERSION="$PACKAGE_VERSION" perl -i -pe \
's/"version": ".*?"/"version": "$ENV{PACKAGE_VERSION}"/g' ui/artalk/package.json
# modify the version in docs
perl -pi -e 's#"latest"(\W+)?:(\W+)?".*?"#"latest": "'${VERSION#v}'"#g' docs/docs/code/ArtalkVersion.json
PACKAGE_VERSION="$PACKAGE_VERSION" perl -pi -e \
's#"latest"(\W+)?:(\W+)?".*?"#"latest": "$ENV{PACKAGE_VERSION}"#g' docs/docs/code/ArtalkVersion.json
- name: Validate Release Changes
run: |
PACKAGE_VERSION="${VERSION#v}"
APP_VERSION="$(
sed -n 's/^var Version = "\(v[0-9][0-9.]*\)"$/\1/p' internal/config/version.go
)"
UI_VERSION="$(jq -r '.version' ui/artalk/package.json)"
DOCS_VERSION="$(jq -r '.latest' docs/docs/code/ArtalkVersion.json)"
if [[ "$APP_VERSION" != "$VERSION" ||
"$UI_VERSION" != "$PACKAGE_VERSION" ||
"$DOCS_VERSION" != "$PACKAGE_VERSION" ]]; then
echo "Generated release versions are inconsistent: app=$APP_VERSION ui=$UI_VERSION docs=$DOCS_VERSION."
exit 1
fi
changelog_entries="$(grep -cF "## [$VERSION]" CHANGELOG.md || true)"
if [[ "$changelog_entries" != "1" ]]; then
echo "Expected one CHANGELOG entry for $VERSION, found $changelog_entries."
exit 1
fi
first_changelog_version="$(
sed -n 's/^## \[\(v[0-9][^]]*\)\].*/\1/p' CHANGELOG.md | head -n 1
)"
if [[ "$first_changelog_version" != "$VERSION" ]]; then
echo "The first CHANGELOG release is $first_changelog_version, expected $VERSION."
exit 1
fi
expected_changes="$(
printf '%s\n' \
CHANGELOG.md \
docs/docs/code/ArtalkVersion.json \
internal/config/version.go \
ui/artalk/package.json |
sort
)"
actual_changes="$(git diff --name-only | sort)"
if [[ "$actual_changes" != "$expected_changes" ]]; then
echo "Unexpected release file set:"
git diff --name-only
exit 1
fi
git diff --check
- name: Print Git Diff
run: git diff
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
if: ${{ !inputs.dry_run }}
with:
branch: "release/${{ env.VERSION }}"
+13 -1
View File
@@ -10,11 +10,23 @@ on:
required: true
default: ""
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Validate event type
env:
EVENT_TYPE: ${{ inputs.event_type }}
run: |
if [[ ! "$EVENT_TYPE" =~ ^[A-Za-z0-9_.-]{1,100}$ ]]; then
echo "Invalid repository dispatch event type."
exit 1
fi
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v3
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
event-type: ${{ inputs.event_type }}
+64 -17
View File
@@ -2,30 +2,69 @@ name: Test App
on:
push:
paths: ["cmd/**", "internal/**", "server/**", "test/**", "main.go", "go.mod", "go.sum"]
paths:
- ".github/workflows/test-app.yml"
- "cmd/**"
- "internal/**"
- "server/**"
- "test/**"
- "main.go"
- "go.mod"
- "go.sum"
branches-ignore: ["release/*", "nightly"]
tags-ignore: ["v*"]
pull_request:
paths: ["cmd/**", "internal/**", "server/**", "test/**", "main.go", "go.mod", "go.sum"]
paths:
- ".github/workflows/test-app.yml"
- "cmd/**"
- "internal/**"
- "server/**"
- "test/**"
- "main.go"
- "go.mod"
- "go.sum"
branches-ignore: ["release/*", "nightly"]
env:
REPORT_DIR: "/tmp/test-reports"
CGO_ENABLED: 0
permissions:
contents: read
concurrency:
group: test-app-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test_app:
name: Test Go on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
name: Test Go (${{ matrix.name }})
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
include:
- name: Linux AMD64
runner: ubuntu-24.04
artifact: linux-amd64
coverage: true
- name: Linux ARM64
runner: ubuntu-24.04-arm
artifact: linux-arm64
coverage: false
- name: macOS
runner: macos-latest
artifact: macos
coverage: false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false # https://github.com/actions/setup-go/issues/483
@@ -37,35 +76,43 @@ jobs:
echo "GOMODCACHE=$(go env GOMODCACHE)" >> "$GITHUB_OUTPUT"
- name: Cache go modules
uses: actions/cache@v4
# v6.1.0; this test-only cache never feeds a publishing workflow.
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # zizmor: ignore[cache-poisoning]
with:
path: |
${{ steps.go-vars.outputs.GOCACHE }}
${{ steps.go-vars.outputs.GOMODCACHE }}
key: go-test-${{ runner.os }}-${{ hashFiles('**/go.mod') }}
key: go-test-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('go.sum') }}
restore-keys: |
go-test-${{ runner.os }}
go-test-${{ runner.os }}-${{ runner.arch }}-
- name: Install tools
run: go install gotest.tools/gotestsum@latest
run: go install gotest.tools/gotestsum@v1.13.0
- name: Download Go modules
run: go mod download
- name: Run vet
run: go vet ./...
- name: Run tests
run: |
mkdir -p $REPORT_DIR
gotestsum --junitfile $REPORT_DIR/unit-tests.xml --jsonfile $REPORT_DIR/unit-tests.json -- -coverprofile=$REPORT_DIR/coverage.out ./...
mkdir -p "$REPORT_DIR"
gotestsum \
--junitfile "$REPORT_DIR/unit-tests.xml" \
--jsonfile "$REPORT_DIR/unit-tests.json" \
-- -coverprofile="$REPORT_DIR/coverage.out" ./...
- name: Upload test results
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Go-Test-Results-${{ runner.os }}
name: Go-Test-Results-${{ matrix.artifact }}
path: ${{ env.REPORT_DIR }}
- if: matrix.os == 'ubuntu-latest'
- if: ${{ matrix.coverage && !cancelled() }}
name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
flags: go
file: ${{ env.REPORT_DIR }}/coverage.out
+16 -7
View File
@@ -9,27 +9,36 @@ on:
paths: ["docs/**"]
branches-ignore: ["release/*", "nightly"]
permissions:
contents: read
concurrency:
group: test-docs-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test_docs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Use pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 9.10.0
version: 10.33.2
- name: Use Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20.x
cache: pnpm
node-version: 22.x
cache: pnpm # zizmor: ignore[cache-poisoning] test-only cache
- name: Install dependencies
run: pnpm install
run: pnpm install --frozen-lockfile
- name: Build Docs
run: pnpm build:docs
+86
View File
@@ -0,0 +1,86 @@
name: Test End-to-End
on:
push:
paths:
- ".github/workflows/test-e2e.yml"
- "cmd/**"
- "conf/**"
- "internal/**"
- "server/**"
- "ui/**"
- "go.mod"
- "go.sum"
- "package.json"
- "pnpm-lock.yaml"
branches-ignore: ["release/*", "nightly"]
tags-ignore: ["v*"]
pull_request:
paths:
- ".github/workflows/test-e2e.yml"
- "cmd/**"
- "conf/**"
- "internal/**"
- "server/**"
- "ui/**"
- "go.mod"
- "go.sum"
- "package.json"
- "pnpm-lock.yaml"
branches-ignore: ["release/*", "nightly"]
permissions:
contents: read
concurrency:
group: test-e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
CGO_ENABLED: 0
jobs:
test_e2e:
name: Test browser UAT flow
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: true # zizmor: ignore[cache-poisoning] test-only cache
- name: Setup pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.2
- name: Setup Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22.x
cache: pnpm # zizmor: ignore[cache-poisoning] test-only cache
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Chromium
run: pnpm exec playwright install --with-deps chromium
- name: Run end-to-end tests
run: pnpm -F artalk test:e2e
- name: Upload Playwright report
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Playwright-Report
path: ui/artalk/playwright-report
if-no-files-found: ignore
retention-days: 7
+40 -12
View File
@@ -2,22 +2,38 @@ name: Test Frontend
on:
push:
paths: ["ui/**"]
paths: [".github/workflows/test-frontend.yml", "ui/**", "package.json", "pnpm-lock.yaml"]
branches-ignore: ["release/*", "nightly"]
tags-ignore: ["v*"]
pull_request:
paths: ["ui/**"]
paths: [".github/workflows/test-frontend.yml", "ui/**", "package.json", "pnpm-lock.yaml"]
branches-ignore: ["release/*", "nightly"]
permissions:
contents: read
concurrency:
group: test-frontend-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
test_ui:
strategy:
fail-fast: false
matrix:
node: [18, 20]
os: [ubuntu-latest, windows-latest]
include:
- os: ubuntu-24.04
node: 22
quality: true
- os: ubuntu-24.04
node: 24
quality: false
- os: windows-latest
node: 22
quality: false
runs-on: ${{ matrix.os }}
timeout-minutes: 20
steps:
- if: matrix.os == 'windows-latest'
@@ -27,17 +43,19 @@ jobs:
git config --global core.eol lf
- name: Checkout Code
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
version: 9.10.0
persist-credentials: false
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 10.33.2
- name: Use Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node }}
cache: pnpm
cache: pnpm # zizmor: ignore[cache-poisoning] test-only cache
- name: Install Dependencies
run: pnpm install --frozen-lockfile
@@ -49,12 +67,22 @@ jobs:
run: pnpm test
- name: Plugins Build Test
if: ${{ matrix.quality }}
run: pnpm build:plugins
- name: Plugins Type Test
if: ${{ matrix.quality }}
run: pnpm test:plugins
- name: Plugins Package Test
if: ${{ matrix.quality }}
run: pnpm test:plugins:attw
- name: Lint
if: ${{ matrix.quality }}
run: pnpm eslint ui && pnpm prettier --check ui
# https://github.com/arethetypeswrong/arethetypeswrong.github.io
- name: "arethetypeswrong test"
if: runner.os == 'ubuntu-latest'
run: pnpm -F artalk test:attw || true
if: ${{ matrix.quality }}
run: pnpm -F artalk test:attw
+46
View File
@@ -0,0 +1,46 @@
name: Test Workflows
on:
push:
paths: [".github/**"]
branches-ignore: ["release/*", "nightly"]
tags-ignore: ["v*"]
pull_request:
paths: [".github/**"]
branches-ignore: ["release/*", "nightly"]
permissions:
contents: read
concurrency:
group: test-workflows-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Validate Actions and security policy
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: go.mod
cache: false
- name: Validate workflow syntax
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 -color
- name: Install zizmor
run: |
python3 -m venv "$RUNNER_TEMP/zizmor"
"$RUNNER_TEMP/zizmor/bin/pip" install --disable-pip-version-check zizmor==1.28.0
- name: Audit workflow security
run: |
"$RUNNER_TEMP/zizmor/bin/zizmor" --format github .github
+3
View File
@@ -69,3 +69,6 @@ node_modules
playwright-report
test-results
# TypeScript composite build declaration output
.tsbuildinfo-dts/
+9 -26
View File
@@ -6,11 +6,13 @@
# https://goreleaser.com/customization/fury/
# https://goreleaser.com/customization/announce/telegram/
version: 2
project_name: artalk
env:
- GO111MODULE=on
- CGO_ENABLED=1
- CGO_ENABLED=0
before:
hooks:
@@ -31,9 +33,6 @@ builds:
- linux
goarch:
- amd64
env:
- CC=x86_64-linux-gnu-gcc
- CXX=x86_64-linux-gnu-g++
binary: "{{.ProjectName}}"
main: ./main.go
ldflags: &common_ldflags |
@@ -45,9 +44,6 @@ builds:
- linux
goarch:
- arm64
env:
- CC=aarch64-linux-gnu-gcc
- CXX=aarch64-linux-gnu-g++
binary: "{{.ProjectName}}"
main: ./main.go
ldflags: *common_ldflags
@@ -60,9 +56,6 @@ builds:
- arm
goarm:
- "7"
env:
- CC=arm-linux-gnueabihf-gcc
- CXX=arm-linux-gnueabihf-g++
binary: "{{.ProjectName}}"
main: ./main.go
ldflags: *common_ldflags
@@ -77,9 +70,6 @@ builds:
- darwin
goarch:
- amd64
env:
- CC=o64-clang
- CXX=o64-clang++
binary: "{{.ProjectName}}"
main: ./main.go
ldflags: *common_ldflags
@@ -90,9 +80,6 @@ builds:
- darwin
goarch:
- arm64
env:
- CC=oa64-clang
- CXX=oa64-clang++
binary: "{{.ProjectName}}"
main: ./main.go
ldflags: *common_ldflags
@@ -107,9 +94,6 @@ builds:
- windows
goarch:
- amd64
env:
- CC=x86_64-w64-mingw32-gcc
- CXX=x86_64-w64-mingw32-g++
binary: "{{.ProjectName}}"
main: ./main.go
ldflags: *common_ldflags
@@ -125,9 +109,6 @@ builds:
- windows
goarch:
- arm64
env:
- CC=/llvm-mingw/bin/aarch64-w64-mingw32-gcc
- CXX=/llvm-mingw/bin/aarch64-w64-mingw32-g++
binary: "{{.ProjectName}}"
main: ./main.go
ldflags: *common_ldflags
@@ -135,7 +116,7 @@ builds:
archives:
- id: artalk
builds:
ids:
- linux-amd64
- darwin-amd64
- windows-amd64
@@ -144,10 +125,12 @@ archives:
- linux-arm64
- darwin-arm64
name_template: "{{.ProjectName}}_v{{.Version}}_{{.Os}}_{{.Arch}}{{.Arm}}"
format: tar.gz
formats:
- tar.gz
format_overrides:
- goos: windows
format: zip
formats:
- zip
wrap_in_directory: true
files:
- README*
@@ -158,7 +141,7 @@ checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{.Version}}-SNAPSHOT-{{.ShortCommit}}"
version_template: "{{.Version}}-SNAPSHOT-{{.ShortCommit}}"
# changelog:
# sort: asc
+2 -2
View File
@@ -1,6 +1,6 @@
{
// Enable the ESlint flat config support
"eslint.experimental.useFlatConfig": true,
// Enable the ESLint flat config support (ESLint 9+ / extension v3+)
"eslint.useFlatConfig": true,
// Disable the default formatter, use eslint instead
// "prettier.enable": false,
+99
View File
@@ -1,4 +1,103 @@
## [v2.10.0](https://github.com/ArtalkJS/Artalk/compare/v2.9.1...v2.10.0) (2026-07-23)
### Features
* add 'ugc' to link rel attributes ([#1120](https://github.com/ArtalkJS/Artalk/issues/1120))
* **api/vote:** add get vote status endpoint ([#998](https://github.com/ArtalkJS/Artalk/issues/998))
* **auth:** add SSO token exchange endpoint ([#1134](https://github.com/ArtalkJS/Artalk/issues/1134))
* **config:** enhance config file and data dir retrieval ([#992](https://github.com/ArtalkJS/Artalk/issues/992))
* **db:** add TLS support for MySQL with Google Cloud SQL ([#1104](https://github.com/ArtalkJS/Artalk/issues/1104))
* **go:** upgrade golang to v1.26.2 and some deps
* **go:** upgrade golang to v1.26.5
* **plugins:** add plugin registry fetch script
* **ui:** add dependency injection container ([#1006](https://github.com/ArtalkJS/Artalk/issues/1006))
* **ui:** add npm published version check script
* **ui/conf:** add `preferRemoteConf` option ([#1000](https://github.com/ArtalkJS/Artalk/issues/1000)) ([#1011](https://github.com/ArtalkJS/Artalk/issues/1011))
* **ui/eslint:** add eslint plugin for artalk ui development ([#995](https://github.com/ArtalkJS/Artalk/issues/995))
* **ui/eslint:** update `eslint-plugin-artalk` to v1.0.2 ([#1010](https://github.com/ArtalkJS/Artalk/issues/1010))
* **ui/page_vote:** add page voting feature ([#983](https://github.com/ArtalkJS/Artalk/issues/983))
### Bug Fixes
* **api:** validate vote status targets
* **api:** make vote updates transactional
* **api/stats:** exclude pending comments
* **api/vote:** return not found for deleted targets
* **auth:** require verified email for SSO exchange
* **build:** propagate image version metadata
* **cache:** return database results on store failure
* **ci:** satisfy workflow shell checks
* **ci:** harden release supply chain
* **ci:** harden release tag handling
* **ci:** preserve nightly tag during release refresh
* **cmd:** preserve config lookup before data chdir
* **config:** preserve keyword file separators
* **core:** initialize process timezone once
* **db:** enforce Cloud SQL TLS verification
* **docs:** support React 19 useRef types
* **email:** add Message-ID header
* **scripts:** import Buffer explicitly
* **scripts:** harden npm publish check
* **ui:** preserve generated list response contract
* **ui:** restore deprecated context services
* **ui:** reject direct context replacement
* **ui:** align plugin lifecycle with final config
* **ui:** accept scoped comment lists without page data
* **ui:** replace vulnerable HTML sanitizer
* **ui/editor:** restore deprecated aliases
* **ui/sidebar:** handle null array preferences
* **ui/types:** preserve atomic config values
* **ui/types:** restore deprecated public aliases
* **ui/ua:** support OpenHarmony and more browsers ([#1087](https://github.com/ArtalkJS/Artalk/issues/1087))
### Code Refactoring
* **api/vote:** refactor vote http endpoint and logic ([#997](https://github.com/ArtalkJS/Artalk/issues/997))
* **auth:** migrate JWT handling to v5
* **ci:** accelerate nightly publishing
* **ci:** optimize multi-architecture releases
* **ui:** improve modularity, lifecycle, and types ([#1007](https://github.com/ArtalkJS/Artalk/issues/1007))
* **ui/auth:** ensure compatibility with new client types ([#1009](https://github.com/ArtalkJS/Artalk/issues/1009))
* **ui/comment:** update reply button to use component
* **ui/conf:** introduce `DeepPartial` type for ui config ([#996](https://github.com/ArtalkJS/Artalk/issues/996))
* **ui/sidebar:** ensure compatibility with new client types ([#1008](https://github.com/ArtalkJS/Artalk/issues/1008))
### Testing
* remove races from async tests
* **ci:** expand release validation
* **server:** verify plugin URL trust filtering
* **ui:** update comment e2e flow
* **ui:** isolate Playwright end-to-end specs
* **ui:** cover Marked 18 integration
* **ui:** cover sanitizer attribute allowlist
* **ui:** isolate browser storage from Node
### Documentation
* document dependency and security updates
* add v2.10.0 release notes
* add Japanese README ([#1145](https://github.com/ArtalkJS/Artalk/issues/1145))
* integrate git changelog plugin
* **i18n:** add Turkish (tr-TR) language support ([#1119](https://github.com/ArtalkJS/Artalk/issues/1119))
* **i18n:** add `zh-TW` translation for config template
* **landing:** redesign landing page ([#1020](https://github.com/ArtalkJS/Artalk/issues/1020))
* **landing:** add more translations
* **landing:** optimize responsive font size
### BREAKING CHANGE
Beyond [#997](https://github.com/ArtalkJS/Artalk/issues/997), [#1007](https://github.com/ArtalkJS/Artalk/issues/1007), and [#1011](https://github.com/ArtalkJS/Artalk/issues/1011), v2.10.0 changes default config/data discovery and relative-path semantics; raises source builds to Go 1.26.5; replaces the CGO-backed SQLite driver with a pure-Go implementation; fixes application timezone at process initialization, requiring a full restart to change it; removes the useBackendConf opt-out and remoteConfModifier; changes DI registration, Context mutation, update() return value, and created timing; and requires marked 18 with modern package resolution. Migration guide: https://artalk.js.org/en/guide/releases/v2.10.0.html
UI client now prioritizes local config in the latest version, whereas previously, the remote config would override it. (See [#1011](https://github.com/ArtalkJS/Artalk/issues/1011))
This update changes the UI client's export Types. Although we aim to maintain backward compatibility, it may still affect downstream UI programs. Please refer to the [TypeDoc](https://artalk.js.org/typedoc/).
Vote HTTP API endpoint have been updated. (See [#997](https://github.com/ArtalkJS/Artalk/issues/997))
## [v2.9.1](https://github.com/ArtalkJS/Artalk/compare/v2.9.0...v2.9.1) (2024-09-18)
### Features
+5 -12
View File
@@ -24,9 +24,9 @@ cd Artalk
To develop Artalk, both frontend and backend, install the following tools:
- [Node.js](https://nodejs.org/en/) (>= 20.17.0)
- [PNPM](https://pnpm.io/) (>= 9.10.0)
- [Go](https://golang.org/) (>= 1.22)
- [Node.js](https://nodejs.org/en/) (>= 22.19.0)
- [PNPM](https://pnpm.io/) (>= 10.33.2)
- [Go](https://golang.org/) (>= 1.26)
- [Docker](https://www.docker.com/) (>= 20.10.0) (optional)
- [Docker Compose](https://docs.docker.com/compose/) (>= 1.29.0) (optional)
@@ -35,30 +35,25 @@ To develop Artalk, both frontend and backend, install the following tools:
The development workflow consists of the following steps:
1. **Set Up Complete Development Instance**:
- Navigate to the Artalk Repo directory.
- Execute `make dev` to run the backend on port `23366`.
- Execute `pnpm dev` to run the frontend on port `5173`.
- Optionally, execute `pnpm dev:sidebar` to run the sidebar frontend on port `23367`.
2. **Access Frontend for Development**:
- Open your browser and go to `http://localhost:5173` to perform your development and testing.
3. **Testing**:
- Run the backend unit tests using `make test`.
- Run the ui unit tests using `pnpm test`.
- Run the ui E2E tests using `make test-frontend-e2e`.
4. **Building Frontend Code**:
- When you make changes to the frontend code, build the complete frontend program using `pnpm build:all`.
- The JavaScript and CSS code will be found in `ui/artalk/dist`.
- The frontend code is automatically deployed to NPM by GitHub Actions.
5. **Building Backend Code**:
- When you make changes to the backend code, note that the backend program embeds the frontend code.
- Before building the backend, run `make build-frontend` (which runs `scripts/build-frontend.sh`). This will place the embedded frontend main program and sidebar frontend program in `/public`.
- Then, build the backend program using `make build`.
@@ -102,13 +97,11 @@ Artalk is a monorepo project, meaning all the source code resides in a single re
### Run Backend
1. **Create Configuration File**:
- Copy `./conf/artalk.example.yml` to the root directory.
- Rename it to `artalk.yml`.
- Modify the file as needed.
2. **Start Backend Program**:
- Run `make dev` to start the backend on port `23366`.
- Access it via `http://localhost:23366`.
- It's recommended to keep the default port for testing.
@@ -124,7 +117,7 @@ If you are using the VSCode, you can use the `F5` key (or RUN button) to start d
Execute `git fetch --tags` to get the latest git tag information. This tag will be used as the version number of the backend app. Alternatively, specify it using environment variables: `VERSION="v1.0.0"` and `COMMIT_HASH="66128e"`.
2. **Build Frontend**:
Run `make build-fronted` to build the frontend, as the backend app embeds the frontend code.
Run `make build-frontend` to build the frontend, as the backend app embeds the frontend code.
3. **Build Backend**:
Execute `make build` to build the backend program.
@@ -224,7 +217,7 @@ Replace `TAG` with the desired tag name (e.g., `latest`).
If you have already built the frontend outside the Docker container, you can skip the frontend build process inside the container to speed up the build. Use the following command:
```sh
docker build --build-arg SKIP_FRONTEND_BUILD=true -t artalk:latest .
docker build --build-arg SKIP_UI_BUILD=true -t artalk:latest .
```
For more details, refer to the `Dockerfile`.
+6 -6
View File
@@ -1,11 +1,11 @@
### build Artalk
FROM golang:1.22.7-alpine3.20 AS builder
FROM golang:1.26.5-alpine3.24 AS builder
WORKDIR /source
# install tools
RUN set -ex \
&& apk add --no-cache make git gcc musl-dev bash
&& apk add --no-cache make git bash
# download go deps
# (cache by separating the downloading of deps)
@@ -22,7 +22,7 @@ ARG SKIP_UI_BUILD=false
RUN set -ex \
&& if [ "$SKIP_UI_BUILD" = "false" ]; then \
apk add --no-cache nodejs npm \
&& npm install -g pnpm@9.10.0 \
&& npm install -g pnpm@10.33.2 \
;fi
RUN set -ex \
@@ -35,12 +35,12 @@ ARG APP_VERSION=""
ARG APP_COMMIT_HASH=""
RUN set -ex \
&& if [[ ! -z "$APP_VERSION" ]]; then export VERSION=$APP_VERSION ;fi \
&& if [[ ! -z "$APP_COMMIT_HASH" ]]; then export COMMIT_HASH=$APP_COMMIT_HASH ;fi \
&& if [[ -n "$APP_VERSION" ]]; then export VERSION="$APP_VERSION" ;fi \
&& if [[ -n "$APP_COMMIT_HASH" ]]; then export COMMIT_HASH="$APP_COMMIT_HASH" ;fi \
&& make build
### build final image
FROM alpine:3.20
FROM alpine:3.24
# we set the timezone `Asia/Shanghai` by default, you can be modified
# by `docker build --build-arg="TZ=Other_Timezone ..."`
+11 -2
View File
@@ -1,11 +1,20 @@
PKG_NAME := github.com/artalkjs/artalk/v2
BIN_NAME := ./bin/artalk
LDFLAGS := -s -w
ifneq ($(strip $(VERSION)),)
LDFLAGS += -X '$(PKG_NAME)/internal/config.Version=$(VERSION)'
endif
ifneq ($(strip $(COMMIT_HASH)),)
LDFLAGS += -X '$(PKG_NAME)/internal/config.buildCommitHash=$(COMMIT_HASH)'
endif
HAS_RICHGO := $(shell which richgo)
GOTEST ?= $(if $(HAS_RICHGO), richgo test, go test)
ARGS ?= server
export CGO_ENABLED := 1
export CGO_ENABLED := 0
all: install build
@@ -17,7 +26,7 @@ run: all
build:
go build \
-ldflags "-s -w" \
-ldflags "$(LDFLAGS)" \
-o $(BIN_NAME) \
$(PKG_NAME)
+134
View File
@@ -0,0 +1,134 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/22412567/171680920-6e74b77c-c565-487b-bff1-4f94976ecbe7.png" alt="Artalk" width="100%">
</p>
# Artalk
[![npm version](https://img.shields.io/npm/v/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![npm downloads](https://img.shields.io/npm/dt/artalk.svg?style=flat-square)](https://www.npmjs.com/package/artalk)
[![Docker Pulls](https://img.shields.io/docker/pulls/artalk/artalk-go?style=flat-square)](https://hub.docker.com/r/artalk/artalk-go)
[![Go Reference](https://pkg.go.dev/badge/github.com/artalkjs/artalk/v2.svg)](https://pkg.go.dev/github.com/artalkjs/artalk/v2)
[![Go Report Card](https://goreportcard.com/badge/github.com/ArtalkJS/Artalk?style=flat-square)](https://goreportcard.com/report/github.com/ArtalkJS/Artalk)
[![CircleCI](https://img.shields.io/circleci/build/gh/ArtalkJS/Artalk?style=flat-square)](https://circleci.com/gh/ArtalkJS/Artalk/tree/master)
[![Codecov](https://img.shields.io/codecov/c/gh/ArtalkJS/Artalk?style=flat-square)](https://codecov.io/gh/ArtalkJS/Artalk)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/artalk?style=flat-square)](https://bundlephobia.com/package/artalk)
[ホームページ](https://artalk.js.org) • [ドキュメント](https://artalk.js.org/en/guide/deploy.html) • [最新リリース](https://github.com/ArtalkJS/Artalk/releases) • [変更履歴](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md) • [English](./README.md) • [简体中文](./README.zh.md)
Artalk は直感的かつ高機能なコメントシステムで、あらゆるブログ、ウェブサイト、ウェブアプリケーションにすぐに導入できます。
- 🍃 クライアントは約 40KB、純粋な Vanilla JS で作られており、フレームワークに依存しません
- 🍱 サーバーは Golang を採用し、効率的かつ軽量なクロスプラットフォーム性能を提供します
- 🐳 Docker によるワンクリックデプロイで、手軽かつ高速に導入できます
- 🌈 オープンソースソフトウェアで、プライバシーを最優先にセルフホストできます
## 機能
<!-- prettier-ignore-start -->
<!-- features -->
* [サイドバー](https://artalk.js.org/guide/frontend/sidebar.html): 素早い管理と直感的な閲覧
* [ソーシャルログイン](https://artalk.js.org/guide/frontend/auth.html): ソーシャルアカウントによる高速ログイン
* [メール通知](https://artalk.js.org/guide/backend/email.html): 多様な送信方法とメールテンプレート
* [多様なプッシュ通知](https://artalk.js.org/guide/backend/admin_notify.html): 複数のプッシュ方法と通知テンプレート
* [サイト内通知](https://artalk.js.org/guide/frontend/sidebar.html): 未読マークとメンション一覧
* [キャプチャ](https://artalk.js.org/guide/backend/captcha.html): 多様な認証タイプと頻度制限
* [コメントモデレーション](https://artalk.js.org/guide/backend/moderator.html): コンテンツ検出とスパム遮断
* [画像アップロード](https://artalk.js.org/guide/backend/img-upload.html): カスタムアップロード、画像ホスティング対応
* [Markdown](https://artalk.js.org/guide/intro.html): Markdown 構文に対応
* [絵文字パック](https://artalk.js.org/guide/frontend/emoticons.html): OwO 互換、簡単に統合
* [マルチサイト](https://artalk.js.org/guide/backend/multi-site.html): サイトの分離と一元管理
* [管理者](https://artalk.js.org/guide/backend/multi-site.html): パスワード認証とバッジ識別
* [ページ管理](https://artalk.js.org/guide/frontend/sidebar.html): 素早い閲覧とワンクリックでのタイトル遷移
* [ページビュー統計](https://artalk.js.org/guide/frontend/pv.html): ページビューを簡単に追跡
* [階層構造](https://artalk.js.org/guide/frontend/config.html#nestmax): ネストされたページネーション一覧と無限スクロール
* [コメント投票](https://artalk.js.org/guide/frontend/config.html#vote): コメントの賛成・反対投票
* [コメント並び替え](https://artalk.js.org/guide/frontend/config.html#listsort): 多様な並び替えオプションを自由に選択
* [コメント検索](https://artalk.js.org/guide/frontend/sidebar.html): コメント内容を素早く検索
* [コメントのピン留め](https://artalk.js.org/guide/frontend/sidebar.html): 重要なメッセージをピン留め
* [作成者のみ表示](https://artalk.js.org/guide/frontend/config.html): 作成者のコメントのみを表示
* [コメントジャンプ](https://artalk.js.org/guide/intro.html): 引用されたコメントへ素早く移動
* [自動保存](https://artalk.js.org/guide/frontend/config.html): 入力内容の消失を防止
* [IP 地域表示](https://artalk.js.org/guide/frontend/ip-region.html): ユーザーの IP 地域を表示
* [データ移行](https://artalk.js.org/guide/transfer.html): 自由な移行と素早いバックアップ
* [画像ライトボックス](https://artalk.js.org/guide/frontend/lightbox.html): 画像ライトボックスを簡単に統合
* [画像の遅延読み込み](https://artalk.js.org/guide/frontend/img-lazy-load.html): 画像を遅延読み込みし、体験を最適化
* [LaTeX](https://artalk.js.org/guide/frontend/latex.html): LaTeX 数式の解析を統合
* [ナイトモード](https://artalk.js.org/guide/frontend/config.html#darkmode): ナイトモードへの切り替え
* [拡張プラグイン](https://artalk.js.org/develop/plugin.html): さらなる可能性を創出
* [多言語対応](https://artalk.js.org/guide/frontend/i18n.html): 複数の言語を切り替え
* [コマンドライン](https://artalk.js.org/guide/backend/config.html): コマンドラインによる操作管理
* [API ドキュメント](https://artalk.js.org/http-api.html): OpenAPI 形式のドキュメントを提供
* [プログラムのアップグレード](https://artalk.js.org/guide/backend/update.html): バージョンチェックとワンクリックアップグレード
<!-- /features -->
<!-- prettier-ignore-end -->
## インストール
Docker でワンステップで Artalk サーバーをデプロイ:
```bash
docker run -d \
--name artalk \
-p 8080:23366 \
-v $(pwd)/data:/data \
-e "TZ=America/New_York" \
-e "ATK_LOCALE=en" \
-e "ATK_SITE_DEFAULT=Artalk Blog" \
-e "ATK_SITE_URL=https://example.com" \
artalk/artalk-go
```
Artalk クライアントをウェブページに組み込む:
<!-- prettier-ignore-start -->
```ts
Artalk.init({
el: '#Comments',
site: 'Artalk Blog',
server: 'https://artalk.example.com',
pageKey: '/2018/10/02/hello-world.html'
})
```
<!-- prettier-ignore-end -->
バイナリファイル、go install、Linux ディストリビューション向けのパッケージマネージャーなど、さまざまなインストール方法を提供しています。
[**詳しく見る →**](https://artalk.js.org/en/guide/deploy.html)
## 開発者の方へ
プルリクエストを歓迎します!
コードベースの扱い方、ローカル開発環境のセットアップ、変更の貢献については [開発](https://artalk.js.org/en/develop/) と [コントリビューション](./CONTRIBUTING.md) をご覧ください。
## コントリビューター
皆さんの貢献はオープンソースコミュニティを豊かにし、学び、ひらめき、イノベーションを育みます。私たちは皆さんの参加を心から大切にしています。私たちのコミュニティに欠かせない存在でいてくれて、ありがとうございます!🥰
[![](https://contrib.rocks/image?repo=ArtalkJS/Artalk)](https://github.com/ArtalkJS/Artalk/graphs/contributors)
## サポーター
[![Stargazers repo roster for @ArtalkJS/Artalk](https://reporoster.com/stars/ArtalkJS/Artalk)](https://github.com/ArtalkJS/Artalk/stargazers)
## Repobeats アナリティクス
![Alt](https://repobeats.axiom.co/api/embed/a9fc9191ac561bc5a8ee2cddc81e635ecaebafb6.svg 'Repobeats analytics image')
## Stargazer の推移
<a href="https://trendshift.io/repositories/6290" target="_blank"><img src="https://trendshift.io/api/badge/repositories/6290" alt="ArtalkJS%2FArtalk | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![Stargazers over time](https://starchart.cc/ArtalkJS/Artalk.svg)](https://starchart.cc/ArtalkJS/Artalk)
## ライセンス
[MIT](./LICENSE)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_shield)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FArtalkJS%2FArtalk?ref=badge_large)
+1 -1
View File
@@ -13,7 +13,7 @@
[![Codecov](https://img.shields.io/codecov/c/gh/ArtalkJS/Artalk?style=flat-square)](https://codecov.io/gh/ArtalkJS/Artalk)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/artalk?style=flat-square)](https://bundlephobia.com/package/artalk)
[Homepage](https://artalk.js.org) • [Documentation](https://artalk.js.org/en/guide/deploy.html) • [Latest Release](https://github.com/ArtalkJS/Artalk/releases) • [Changelog](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md) • [简体中文](./README.zh.md)
[Homepage](https://artalk.js.org) • [Documentation](https://artalk.js.org/en/guide/deploy.html) • [Latest Release](https://github.com/ArtalkJS/Artalk/releases) • [Changelog](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md) • [简体中文](./README.zh.md) • [日本語](./README.ja.md)
Artalk is an intuitive yet feature-rich comment system, ready for immediate deployment into any blog, website, or web application.
+1 -1
View File
@@ -13,7 +13,7 @@
[![Codecov](https://img.shields.io/codecov/c/gh/ArtalkJS/Artalk?style=flat-square)](https://codecov.io/gh/ArtalkJS/Artalk)
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/artalk?style=flat-square)](https://bundlephobia.com/package/artalk)
[官方网站](https://artalk.js.org) • [最新版本](https://github.com/ArtalkJS/Artalk/releases) • [更新日志](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md) • [English](./README.md)
[官方网站](https://artalk.js.org) • [最新版本](https://github.com/ArtalkJS/Artalk/releases) • [更新日志](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md) • [English](./README.md) • [日本語](./README.ja.md)
Artalk 是一款简单易用但功能丰富的评论系统,你可以开箱即用地部署并置入任何博客、网站、Web 应用。
+29
View File
@@ -5,6 +5,7 @@ import (
"os"
"os/signal"
"path"
"path/filepath"
"syscall"
"github.com/adrg/xdg"
@@ -67,6 +68,13 @@ func New() *ArtalkCmd {
// Parse base flags
cmd.eagerParseFlags()
// Resolve config from the invocation directory before an implicit data-dir chdir.
if cfgFile, err := resolveConfigFileBeforeDataDir(cmd.cfgFile, cmd.workDir); err == nil {
cmd.cfgFile = cfgFile
} else {
log.Fatal("Config path fail: ", err)
}
// Init data directory (work directory)
if workDir, err := initDataDir(cmd.workDir); err == nil {
cmd.workDir = workDir
@@ -77,6 +85,27 @@ func New() *ArtalkCmd {
return cmd
}
func resolveConfigFileBeforeDataDir(cfgFile string, workDir string) (string, error) {
// An explicit work directory keeps the historical behavior: relative config
// paths and default config discovery are resolved after changing to workDir.
if workDir != "" {
return cfgFile, nil
}
if cfgFile == "" {
cfgFile = config.RetrieveConfigFile()
}
if cfgFile == "" || filepath.IsAbs(cfgFile) {
return cfgFile, nil
}
absPath, err := filepath.Abs(cfgFile)
if err != nil {
return "", fmt.Errorf("resolve config file %q: %w", cfgFile, err)
}
return absPath, nil
}
// 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 {
+72
View File
@@ -0,0 +1,72 @@
package cmd
import (
"os"
"path/filepath"
"testing"
"github.com/adrg/xdg"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResolveConfigFileBeforeDataDir(t *testing.T) {
originalDir, err := os.Getwd()
require.NoError(t, err)
originalDataHome := xdg.DataHome
defer func() {
require.NoError(t, os.Chdir(originalDir))
xdg.DataHome = originalDataHome
}()
invocationDir := t.TempDir()
dataHome := t.TempDir()
invocationDir, err = filepath.EvalSymlinks(invocationDir)
require.NoError(t, err)
dataHome, err = filepath.EvalSymlinks(dataHome)
require.NoError(t, err)
dataDir := filepath.Join(dataHome, "artalk")
require.NoError(t, os.Mkdir(dataDir, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(invocationDir, "artalk.yml"),
[]byte("timezone: UTC\n"),
0o600,
))
require.NoError(t, os.Chdir(invocationDir))
xdg.DataHome = dataHome
cfgFile, err := resolveConfigFileBeforeDataDir("", "")
require.NoError(t, err)
assert.Equal(t, filepath.Join(invocationDir, "artalk.yml"), cfgFile)
workDir, err := initDataDir("")
require.NoError(t, err)
assert.Equal(t, dataDir, workDir)
currentDir, err := os.Getwd()
require.NoError(t, err)
assert.Equal(t, dataDir, currentDir)
conf, err := getConfig(cfgFile)
require.NoError(t, err)
assert.Equal(t, "UTC", conf.TimeZone)
}
func TestResolveConfigFileWithExplicitPaths(t *testing.T) {
originalDir, err := os.Getwd()
require.NoError(t, err)
defer func() { require.NoError(t, os.Chdir(originalDir)) }()
invocationDir := t.TempDir()
invocationDir, err = filepath.EvalSymlinks(invocationDir)
require.NoError(t, err)
require.NoError(t, os.Chdir(invocationDir))
cfgFile, err := resolveConfigFileBeforeDataDir("custom.yml", "")
require.NoError(t, err)
assert.Equal(t, filepath.Join(invocationDir, "custom.yml"), cfgFile)
cfgFile, err = resolveConfigFileBeforeDataDir("custom.yml", "./runtime")
require.NoError(t, err)
assert.Equal(t, "custom.yml", cfgFile)
}
+3
View File
@@ -220,6 +220,9 @@ auth:
client_id: ""
client_secret: ""
domain: ""
sso:
enabled: false
issuer: ""
frontend:
placeholder: ""
noComment: ""
+456
View File
@@ -0,0 +1,456 @@
# Dinleme adresi
host: "0.0.0.0"
# Dinleme portu
port: 23366
# Uygulama Anahtarı (JWT üretimi için)
app_key: ""
# Hata ayıklama modu
debug: false
# Dil (Unicode BCP 47 formatında) ["en", "zh-CN", "zh-TW", "ja", "fr", "ko", "ru", "tr"]
# -- bkz. https://www.techonthenet.com/js/language_tags.php --
locale: tr
# Zaman dilimi (IANA Zaman Dilimi Veritabanı formatında)
# -- bkz. https://en.wikipedia.org/wiki/List_of_tz_database_time_zones --
timezone: Europe/Istanbul
# Varsayılan site adı (uygulama ilk başlatıldığında oluşturulur)
site_default: Varsayılan Site
# Varsayılan site URL'si
site_url: ""
# Giriş zaman aşımı (saniye cinsinden)
login_timeout: 259200
# Veritabanı
db:
# Veritabanı türü ["sqlite", "mysql", "pgsql", "mssql"]
type: sqlite
# Veritabanı dosyası (yalnızca SQLite için)
file: ./data/artalk.db
# Tablo öneki (örn. "atk_")
table_prefix: ""
# -- Aşağıdakiler SQLite için gerekli değildir --
# Veritabanı adı
name: artalk
# Ana makine adresi
host: localhost
# Ana makine portu
port: 3306
# Veritabanı kullanıcısı
user: root
# Veritabanı şifresi
password: ""
# Veritabanı karakter seti
charset: utf8mb4
# SSL modunu etkinleştir
ssl: false
# Hazır İfadeler (Prepared Statement)
prepare_stmt: true
# Web sunucusu
http:
# Gövde boyutu sınırı (birim: MB)
body_limit: 100
# Proxy Başlığı (güvenilir bir ters proxy veya CDN arkasındaysa kullanıcının gerçek IP'sini almak için `X-Forwarded-For` doldurun)
proxy_header: ""
# Günlük Kaydı (Logging)
log:
# Günlük kaydını etkinleştir
enabled: true
# Günlük dosyası yolu
filename: ./data/artalk.log
# Önbellek (Cache)
cache:
# Önbelleği etkinleştir
enabled: false
# Önbellek türü ["redis", "memcache", "builtin"]
type: builtin
# Önbellek geçerlilik süresi (dakika cinsinden)
expires: 30
# Önbellek ön yükleme (program başladığında önbelleği ısıtır)
warm_up: false
# -- Aşağıdakiler `builtin` (yerleşik) önbellek için gerekli değildir --
# Önbellek sunucu adresi (örn. "localhost:6379")
server: ""
# Redis yapılandırması
redis:
# Bağlantı türü ["tcp", "unix"]
network: tcp
# Redis kullanıcı adı
username: ""
# Redis şifresi
password: ""
# Redis veritabanı numarası (örn. 0)
db: 0
# Güvenilir alan adları
# -- örn. ["https://artalk.example.com:23366"] sitenizin URL'sini buraya ekleyin --
trusted_domains: []
# SSL
ssl:
# SSL'i etkinleştir
enabled: false
# Sertifika dosyası yolu
# (örn. "/etc/letsencrypt/live/example.com/fullchain.pem")
cert_path: ""
# Anahtar dosyası yolu
# (örn. "/etc/letsencrypt/live/example.com/privkey.pem")
key_path: ""
# Moderatör
# -- Yorumların herkese açık olmadan önce incelenmesi --
moderator:
# Varsayılan bekleme durumu (yeni yorumların yönetici tarafından onaylanması gerekir)
pending_default: false
# API isteği başarısız olduğunda engelle (API isteği başarısız olduğunda yorumların geçmesine izin vermek için false yapın)
api_fail_block: false
# Akismet Anahtarı
# (Akismet spam önleme servisi, https://akismet.com)
akismet_key: ""
# Tencent Bulut İçerik Güvenliği
# (Tencent Bulut İçerik Güvenliği ile yorumları otomatik inceleyin)
# -- bkz. https://cloud.tencent.com/document/product/1124/64508 --
tencent:
enabled: false
secret_id: ""
secret_key: ""
region: ap-guangzhou
# Aliyun İçerik Güvenliği
# (Aliyun İçerik Güvenliği ile yorumları otomatik inceleyin)
# -- bkz. https://help.aliyun.com/document_detail/28417.html --
aliyun:
enabled: false
access_key_id: ""
access_key_secret: ""
region: cn-shanghai
# Kelime filtresi (yerel çevrimdışı sözlük)
keywords:
# Kelime filtresini etkinleştir
enabled: false
# Eşleşme olduğunda beklemeye al
pending: false
# Sözlük dosyası (birden fazla sözlük dosyasını destekler)
files:
- ./data/keywords_1.txt
file_sep: "\n"
replace_to: x
# Doğrulama (Captcha)
captcha:
# Doğrulamayı etkinleştir
enabled: true
# Her zaman doğrulama gerekli
always: false
# Doğrulama türü ["image", "turnstile", "recaptcha", "hcaptcha", "geetest"]
captcha_type: image
# İşlem sınırı
# (doğrulamayı etkinleştirmek için gereken işlem sayısı)
action_limit: 3
# Sıfırlama Zaman Aşımı (işlem sayacını sıfırlama süresi. birim: saniye, devre dışı bırakmak için -1 yapın)
action_reset: 60
# Turnstile
# (https://www.cloudflare.com/products/turnstile/)
turnstile:
site_key: ""
secret_key: ""
# reCaptcha
# (https://www.google.com/recaptcha/about/)
recaptcha:
site_key: ""
secret_key: ""
# hCaptcha (https://www.hcaptcha.com/)
hcaptcha:
site_key: ""
secret_key: ""
# Geetest (https://www.geetest.com)
geetest:
captcha_id: ""
captcha_key: ""
# Yükleme
img_upload:
# Resim yüklemeyi etkinleştir
enabled: true
# Resim depolama yolu
path: ./data/artalk-img/
# Resim boyutu sınırı (birim: MB)
max_size: 5
# Resim bağlantısı temel yolu (varsayılan: "/static/images/")
public_path: null
# Upgit yapılandırması
upgit:
# Upgit'i etkinleştir
enabled: false
# Komut satırı argümanları
exec: upgit -c UPGIT_CONF_FILE_PATH -t /artalk-img
# Yükleme başarılı olduktan sonra yerel resmi sil
del_local: true
# E-posta
email:
# E-posta bildirimini etkinleştir
enabled: false
# Gönderim yöntemi ["smtp", "ali_dm", "sendmail"]
send_type: smtp
# Gönderen takma adı
send_name: "{{reply_nick}}"
# Gönderen e-posta adresi
send_addr: noreply@example.com
# E-posta konusu
mail_subject: "[{{site_name}}] @{{reply_nick}} tarafından bir yanıt aldınız"
# E-posta şablon dosyası (özel şablon kullanmak için dosya yolunu ayarlayın)
mail_tpl: default
# SMTP gönderimi (etkinleştirmek için gönderim yöntemini "smtp" yapın)
smtp:
# Gönderen e-posta sunucusu
host: smtp.qq.com
# E-posta portu
port: 587
# Kullanıcı adı
username: example@qq.com
# Şifre
password: ""
# Aliyun posta push
# (etkinleştirmek için gönderim yöntemini "ali_dm" yapın; bkz: https://help.aliyun.com/document_detail/29444.html)
ali_dm:
access_key_id: ""
access_key_secret: ""
account_name: noreply@example.com
# Çoklu Push
admin_notify:
# Bildirim şablonu (özel şablon kullanmak için dosya yolunu ayarlayın)
notify_tpl: default
# Bekleyen yorumlar için hala bildirim gönder (yorumlar engellendiğinde bile bildirimler gönderilir)
notify_pending: false
# Gürültü modu
# -- gürültü modu varsayılan olarak devre dışıdır. --
# -- Bu seçenek `false` olarak ayarlandığında, yalnızca yöneticiye gönderilen mesajlar bildirilir, --
# -- "kullanıcı A"nın "kullanıcı B"ye yanıt vermesi gibi iki kullanıcı arasındaki iletişim yöneticiye bildirilmez. --
noise_mode: false
# Yöneticiyi bildir
email:
# Etkinleştir (diğer push yöntemlerini kullanırken devre dışı bırakılabilir)
enabled: true
# E-posta konusu (yöneticiye gönderilen e-posta konusu)
mail_subject: '[{{site_name}}] "{{page_title}}" sayfası yeni bir yoruma sahip'
# Yönetici e-posta şablon dosyası (özel şablon kullanmak için dosya yolunu ayarlayın)
mail_tpl: ""
# Telegram
telegram:
enabled: false
api_token: ""
receivers:
- 7777777
# Bark
bark:
enabled: false
server: http://day.app/xxxxxxx/
# Lark
lark:
enabled: false
webhook_url: ""
# Mesaj türü ["text", "card"]
msg_type: "text"
# WebHook
webhook:
enabled: false
url: ""
# DingTalk
ding_talk:
enabled: false
token: ""
secret: ""
# Slack
slack:
enabled: false
oauth_token: ""
receivers:
- "CHANNEL_ID"
# LINE
line:
enabled: false
channel_secret: ""
channel_access_token: ""
receivers:
- USER_ID_1
- GROUP_ID_1
# Sosyal Giriş
auth:
# Sosyal Girişi Etkinleştir
enabled: false
# Anonim yorum yapmaya izin ver (Doğrulamayı atlamaya izin ver, yalnızca anonim bir takma ad ve e-posta doldurmak yeterlidir)
anonymous: false
# Geri dönüş URL'si (https://example.com/api/v2/auth/{provider}/callback)
callback: "http://localhost:23366/api/v2/auth/{provider}/callback"
# E-posta
email:
# E-posta şifre girişini etkinleştir
enabled: true
# Doğrulama e-postası konusu
verify_subject: "Kodunuz - {{code}}"
# Doğrulama e-postası şablonu (özel şablon kullanmak için dosya yolunu ayarlayın)
verify_tpl: default
# GitHub
github:
enabled: false
client_id: ""
client_secret: ""
# GitLab
gitlab:
enabled: false
client_id: ""
client_secret: ""
# Gitea
gitea:
enabled: false
client_id: ""
client_secret: ""
# Google
google:
enabled: false
client_id: ""
client_secret: ""
# Mastodon
mastodon:
enabled: false
client_id: ""
client_secret: ""
# Twitter
twitter:
enabled: false
client_id: ""
client_secret: ""
# Facebook
facebook:
enabled: false
client_id: ""
client_secret: ""
# Discord
discord:
enabled: false
client_id: ""
client_secret: ""
# Steam
steam:
enabled: false
api_key: ""
# Apple
apple:
enabled: false
client_id: ""
client_secret: ""
# Microsoft
microsoft:
enabled: false
client_id: ""
client_secret: ""
# WeChat
wechat:
enabled: false
client_id: ""
client_secret: ""
# Tiktok
tiktok:
enabled: false
client_id: ""
client_secret: ""
# Slack
slack:
enabled: false
client_id: ""
client_secret: ""
# Line
line:
enabled: false
client_id: ""
client_secret: ""
# Patreon
patreon:
enabled: false
client_id: ""
client_secret: ""
# Auth0
auth0:
enabled: false
client_id: ""
client_secret: ""
domain: ""
# SSO (Harici OIDC erişim belirteçlerini Artalk oturumuyla değiştirir, IdP'nin /userinfo uç noktası ile doğrulanır)
# -- Gömülü uygulama zaten OIDC çalıştırıyorsa ve Artalk'ın kendi açılır giriş ekranını göstermeden bu oturumu devralmasını istiyorsanız kullanın. --
sso:
# SSO belirteç değişimini etkinleştir
enabled: false
# OIDC issuer URL'si (örn. "tenant.auth0.com" veya "https://tenant.auth0.com")
issuer: ""
# Kullanıcı Arayüzü Ayarları
frontend:
# Yorum kutusu yer tutucu metni
placeholder: ""
# Yorum olmadığında görüntülenecek metin
noComment: ""
# Gönder düğmesi metni
sendBtn: ""
# Hareketli yorum kutusu
editorTravel: true
# İfadeler (Emoticonlar)
emoticons: https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json
# Oy verme düğmesi
vote: true
# Beğenmeme düğmesi
voteDown: false
# Kullanıcı UA rozeti
uaBadge: false
# Yorum sıralama
listSort: true
# Editör gerçek zamanlı önizleme
preview: true
# Düzleştirme modu ["auto", true, false]
flatMode: auto
# Karanlık mod ["inherit", "auto"]
darkMode: inherit
# Maksimum yuvalama seviyesi
nestMax: 2
# Yuvalama yorum sıralama kuralları ["DATE_ASC", "DATE_DESC", "VOTE_UP_DESC"]
nestSort: DATE_ASC
# Gravatar
gravatar:
# API URL'si
mirror: https://www.gravatar.com/avatar/
# API parametreleri
params: sha256=1&d=mp&s=240
# Yorum sayfalama
pagination:
# Sayfa başına yorum sayısı
pageSize: 20
# Daha fazla yükle modu (sayfalama çubuğunu kullanmak için devre dışı bırakın)
readMore: true
# Kaydırmalı yükleme
autoLoad: true
# İçerik yükseklik sınırı
heightLimit:
# Yorum içeriği yükseklik sınırı (birim: px)
content: 300
# Alt yorum alanı yükseklik sınırı (birim: px)
children: 400
# Kaydırılabilir (yükseklik sınırı alanı kaydırılabilir olsun)
scrollable: false
# Resim geç yükleme [false, "native", "data-src"]
imgLazyLoad: false
# İstek zaman aşımı (birim: ms)
reqTimeout: 15000
# Sürüm kontrolü
versionCheck: true
# Eklentiler
pluginURLs: []
+8 -1
View File
@@ -10,7 +10,7 @@ app_key: ""
# Debug mode
debug: false
# Language (follow Unicode BCP 47) ["en", "zh-CN", "zh-TW", "ja", "fr", "ko", "ru"]
# Language (follow Unicode BCP 47) ["en", "zh-CN", "zh-TW", "ja", "fr", "ko", "ru", "tr"]
# -- see https://www.techonthenet.com/js/language_tags.php --
locale: en
@@ -386,6 +386,13 @@ auth:
client_id: ""
client_secret: ""
domain: ""
# SSO (Exchange external OIDC access tokens for Artalk login sessions, verified via the IdP's /userinfo endpoint)
# -- Use this when the embedding app already runs OIDC and you want Artalk to inherit that session without its own popup login UI. --
sso:
# Enable SSO token exchange
enabled: false
# OIDC issuer URL (e.g. "tenant.auth0.com" or "https://tenant.auth0.com")
issuer: ""
# UI Settings
frontend:
+7
View File
@@ -394,6 +394,13 @@ auth:
client_id: ""
client_secret: ""
domain: ""
# SSO (用外部 OIDC 身份提供商的访问令牌换取 Artalk 登录会话,通过调用 IdP 的 /userinfo 端点校验)
# -- 当嵌入页面已运行 OIDC 且希望 Artalk 复用该登录态、不显示自身弹窗登录界面时使用。 --
sso:
# 启用 SSO 令牌交换
enabled: false
# OIDC issuer 地址 (如 "tenant.auth0.com" 或 "https://tenant.auth0.com")
issuer: ""
# 界面配置
frontend:
+7
View File
@@ -394,6 +394,13 @@ auth:
client_id: ""
client_secret: ""
domain: ""
# SSO (用外部 OIDC 身分提供商的存取權杖換取 Artalk 登入工作階段,透過呼叫 IdP 的 /userinfo 端點驗證)
# -- 當嵌入頁面已執行 OIDC 且希望 Artalk 沿用該登入狀態、不顯示自身彈窗登入介面時使用。 --
sso:
# 啟用 SSO 權杖交換
enabled: false
# OIDC issuer 位址 (如 "tenant.auth0.com" 或 "https://tenant.auth0.com")
issuer: ""
# 介面配置
frontend:
+1
View File
@@ -1,2 +1,3 @@
.vitepress/dist/**
.vitepress/cache/**
.vitepress/.temp/**
+1 -1
View File
@@ -1,3 +1,3 @@
{
"latest": "2.9.1"
"latest": "2.10.0"
}
+2 -2
View File
@@ -105,7 +105,7 @@ import Artalk from 'artalk'
const ArtalkComment = () => {
const { pathname } = useLocation()
const artalk = useRef<Artalk>()
const artalk = useRef<Artalk | null>(null)
const handleContainerInit = useCallback(
(node: HTMLDivElement | null) => {
@@ -114,7 +114,7 @@ const ArtalkComment = () => {
}
if (artalk.current) {
artalk.current.destroy()
artalk.current = undefined
artalk.current = null
}
artalk.current = Artalk.init({
el: node,
+10
View File
@@ -283,6 +283,16 @@ timezone: Asia/Shanghai
Fill in the time zone where you are located, corresponding to the IANA database time zone name, refer to: [Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) / [RFC-6557](https://www.rfc-editor.org/rfc/rfc6557.html).
`timezone` controls the timezone used by the Artalk application. It may differ from the host system or container `TZ` setting. The same configuration can be supplied through `ATK_TIMEZONE`:
```bash
ATK_TIMEZONE=America/New_York artalk server
```
::: warning Restart required
The application timezone is a process-level fixed value and is set only when the Artalk process initializes. It cannot be changed dynamically at runtime. After changing it in the configuration file, environment, or Dashboard, fully restart the Artalk process or container for the new value to take effect.
:::
```
UTC+08:00 Asia/Shanghai
UTC+09:00 Asia/Tokyo
+1
View File
@@ -109,6 +109,7 @@ moderator:
- **pending**: Whether to set the comment to pending review status upon successful match.
- **files**: Keyword files. Multiple files are allowed, and Artalk will merge the keyword libraries upon startup.
- **file_sep**: Keyword file content separator. For example, if each line in the file contains a keyword, set this item to `\n`.
The value cannot be empty. The dashboard displays line feeds, carriage returns, and tabs as `\n`, `\r`, and `\t`. Enter `\\n` when the separator must be the literal characters `\n`.
- **replace_to**: Replacement character. For example, if this item is set to `x`, you can set `pending` to `false`, allowing the comment to pass review automatically, but the matched keywords will be replaced with `x`, resulting in `fxxk` or `xxxx`.
Note: It is recommended not to use `*` asterisk as `replace_to` because it conflicts with the Markdown bold syntax.
+4 -1
View File
@@ -2,7 +2,10 @@
## Update Changes
For significant changes, such as updates that require modifications to existing configurations, please refer to the version update log for relevant prompts: [CHANGELOG.md](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md).
For significant changes, such as updates that require modifications to existing configurations, refer to the release notes and migration instructions:
- [v2.10.0 Release Notes and Migration Guide](../releases/v2.10.0.md)
- [Complete CHANGELOG.md](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md)
## One-Click Upgrade via Command Line
+20 -15
View File
@@ -25,6 +25,7 @@ services:
- ./data:/data
environment:
- TZ=America/New_York
- ATK_TIMEZONE=America/New_York
- ATK_LOCALE=en
- ATK_SITE_DEFAULT=Artalk's Blog
- ATK_SITE_URL=https://example.com
@@ -36,6 +37,8 @@ services:
- ATK_ADMIN_USERS_0_BADGE_COLOR=#0083FF
```
`TZ` controls the container or host process environment, while `ATK_TIMEZONE` controls the Artalk application timezone; the two values may differ. The Artalk timezone is applied only during process initialization, so fully restart the Artalk process or container after changing `ATK_TIMEZONE`.
When the variable is an array, set array values using space-separated strings or numerical indices, for example:
```
@@ -66,14 +69,14 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
<div class="env-table">
<!-- env-variables -->
## 通用配置
## General
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_APP_KEY** | `""` | App Key (for generation of JWT) | app_key (App Key) |
| **ATK_DEBUG** | `false` | Debug mode | debug (Debug mode) |
| **ATK_HOST** | `"0.0.0.0"` | Listen host | host (Listen host) |
| **ATK_LOCALE** | `"en"` | Language (follow Unicode BCP 47) (可选:`["en", "zh-CN", "zh-TW", "ja", "fr", "ko", "ru"]`) | locale (Language) |
| **ATK_LOCALE** | `"en"` | Language (follow Unicode BCP 47) (可选:`["en", "zh-CN", "zh-TW", "ja", "fr", "ko", "ru", "tr"]`) | locale (Language) |
| **ATK_LOGIN_TIMEOUT** | `259200` | Login timeout (in seconds) | login_timeout (Login timeout) |
| **ATK_PORT** | `23366` | Listen port | port (Listen port) |
| **ATK_SITE_DEFAULT** | `"Default Site"` | Default site name (create when app is first launched) | site_default (Default site name) |
@@ -84,7 +87,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Multi-Push
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_ADMIN_NOTIFY_BARK_ENABLED** | `false` | 启用 | admin_notify.bark.enabled (Multi-Push > Bark > Enabled) |
| **ATK_ADMIN_NOTIFY_BARK_SERVER** | `"http://day.app/xxxxxxx/"` | Server | admin_notify.bark.server (Multi-Push > Bark > Server) |
@@ -116,7 +119,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Social Login
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_AUTH_ANONYMOUS** | `false` | Allow anonymous commenting (Allow skipping verification, only fill in an anonymous nickname and email) | auth.anonymous (Social Login > Allow anonymous commenting) |
| **ATK_AUTH_APPLE_CLIENT_ID** | `""` | ClientId | auth.apple.client_id (Social Login > Apple > ClientId) |
@@ -164,6 +167,8 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
| **ATK_AUTH_SLACK_CLIENT_ID** | `""` | ClientId | auth.slack.client_id (Social Login > Slack > ClientId) |
| **ATK_AUTH_SLACK_CLIENT_SECRET** | `""` | ClientSecret | auth.slack.client_secret (Social Login > Slack > ClientSecret) |
| **ATK_AUTH_SLACK_ENABLED** | `false` | 启用 | auth.slack.enabled (Social Login > Slack > Enabled) |
| **ATK_AUTH_SSO_ENABLED** | `false` | Enable SSO token exchange | auth.sso.enabled (Social Login > SSO > Enable SSO token exchange) |
| **ATK_AUTH_SSO_ISSUER** | `""` | OIDC issuer URL (e.g. "tenant.auth0.com" or "https://tenant.auth0.com") | auth.sso.issuer (Social Login > SSO > OIDC issuer URL) |
| **ATK_AUTH_STEAM_API_KEY** | `""` | ApiKey | auth.steam.api_key (Social Login > Steam > ApiKey) |
| **ATK_AUTH_STEAM_ENABLED** | `false` | 启用 | auth.steam.enabled (Social Login > Steam > Enabled) |
| **ATK_AUTH_TIKTOK_CLIENT_ID** | `""` | ClientId | auth.tiktok.client_id (Social Login > Tiktok > ClientId) |
@@ -179,7 +184,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Cache
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_CACHE_ENABLED** | `false` | Enable cache | cache.enabled (Cache > Enable cache) |
| **ATK_CACHE_EXPIRES** | `30` | Cache expiration time (in minutes) | cache.expires (Cache > Cache expiration time) |
@@ -194,7 +199,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Captcha
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_CAPTCHA_ACTION_LIMIT** | `3` | Action limit (the number of actions required to activate captcha) | captcha.action_limit (Captcha > Action limit) |
| **ATK_CAPTCHA_ACTION_RESET** | `60` | Reset Timeout (timeout to reset action counter. unit: s, set to -1 to disable) | captcha.action_reset (Captcha > Reset Timeout) |
@@ -213,7 +218,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Database
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_DB_CHARSET** | `"utf8mb4"` | Database charset | db.charset (Database > Database charset) |
| **ATK_DB_FILE** | `"./data/artalk.db"` | Database file (only for SQLite) | db.file (Database > Database file) |
@@ -230,7 +235,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Email
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_EMAIL_ALI_DM_ACCESS_KEY_ID** | `""` | AccessKeyId | email.ali_dm.access_key_id (Email > Aliyun mail push > AccessKeyId) |
| **ATK_EMAIL_ALI_DM_ACCESS_KEY_SECRET** | `""` | AccessKeySecret | email.ali_dm.access_key_secret (Email > Aliyun mail push > AccessKeySecret) |
@@ -249,7 +254,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## UI Settings
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_FRONTEND_DARKMODE** | `"inherit"` | Dark mode (可选:`["inherit", "auto"]`) | frontend.darkMode (UI Settings > Dark mode) |
| **ATK_FRONTEND_EDITORTRAVEL** | `true` | Movable comment box | frontend.editorTravel (UI Settings > Movable comment box) |
@@ -281,7 +286,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Web server
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_HTTP_BODY_LIMIT** | `100` | Body size limit (unit: MB) | http.body_limit (Web server > Body size limit) |
| **ATK_HTTP_PROXY_HEADER** | `""` | Proxy Header (fill `X-Forwarded-For` to get user real IP if behind a trusted reverse proxy or CDN) | http.proxy_header (Web server > Proxy Header) |
@@ -289,7 +294,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Upload
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_IMG_UPLOAD_ENABLED** | `true` | Enable image upload | img_upload.enabled (Upload > Enable image upload) |
| **ATK_IMG_UPLOAD_MAX_SIZE** | `5` | Image size limit (unit: MB) | img_upload.max_size (Upload > Image size limit) |
@@ -302,7 +307,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Logging
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_LOG_ENABLED** | `true` | Enable logging | log.enabled (Logging > Enable logging) |
| **ATK_LOG_FILENAME** | `"./data/artalk.log"` | Log file path | log.filename (Logging > Log file path) |
@@ -310,7 +315,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## Moderator
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_MODERATOR_AKISMET_KEY** | `""` | Akismet Key (Akismet anti-spam service, https://akismet.com) | moderator.akismet_key (Moderator > Akismet Key) |
| **ATK_MODERATOR_ALIYUN_ACCESS_KEY_ID** | `""` | AccessKeyId | moderator.aliyun.access_key_id (Moderator > Aliyun Content Security > AccessKeyId) |
@@ -332,7 +337,7 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
## SSL
| 环境变量 | 默认值 | 描述 | 路径 |
| Env | Default | Desc | Path |
| --- | --- | --- | --- |
| **ATK_SSL_CERT_PATH** | `""` | Certificate file path (e.g. "/etc/letsencrypt/live/example.com/fullchain.pem") | ssl.cert_path (SSL > Certificate file path) |
| **ATK_SSL_ENABLED** | `false` | Enable SSL | ssl.enabled (SSL > Enable SSL) |
+56
View File
@@ -68,6 +68,62 @@ If only one login method is enabled, such as GitHub login, the GitHub authorizat
For integrating GitHub login, refer to the documentation: [About Creating GitHub Apps](https://docs.github.com/en/developers/apps/building-oauth-apps/creating-an-oauth-app). After obtaining the Client ID and Client Secret, fill them in the "GitHub" option in the social login settings page of the Artalk Dashboard.
## SSO Token Exchange
If your site is embedded in an application that already authenticates users through an external OIDC identity provider (Auth0, Keycloak, Okta, etc.), you can let Artalk reuse that session instead of showing its own login UI. The surrounding application exchanges the IdP access token it already holds for an Artalk session token, so users who are already signed in to the parent app can comment without an extra click or popup.
This feature is **disabled by default** and is opt-in only. Enable it through the [configuration file](../backend/config.md) or [environment variables](../env.md#social-login), filling in the OIDC issuer of your provider:
```yaml
auth:
enabled: true
sso:
enabled: true
issuer: "https://tenant.auth0.com" # e.g. "tenant.auth0.com" or "https://tenant.auth0.com"
```
Once enabled, the surrounding application posts the IdP access token to the exchange endpoint:
```
POST /api/v2/sso/exchange
Content-Type: application/json
{ "token": "<external IdP access token>" }
```
Artalk validates the token by calling the issuer's `/userinfo` endpoint (the standard OIDC way — the IdP verifies and signs the response server-side, so no key handling is required on Artalk's side) and reads the `email` claim. Artalk only finds or creates the matching user when the response also contains `email_verified: true`, then returns the same response shape as the other login endpoints:
```json
{
"token": "<Artalk session token>",
"user": { "name": "...", "email": "...", "is_admin": false }
}
```
The frontend writes this into `localStorage["ArtalkUser"]` before `Artalk.init()` runs, and the widget then treats the user as fully logged in (no popup, and no admin password prompt for admin users):
```js
const res = await fetch('https://comments.example.com/api/v2/sso/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: idpAccessToken }), // the access token from your IdP
})
const { token, user } = await res.json()
// Pre-populate the Artalk session before the widget mounts
localStorage.setItem('ArtalkUser', JSON.stringify({ ...user, token }))
Artalk.init({ el: '#Comments', server: 'https://comments.example.com', site: 'Blog' })
```
The endpoint returns `404` when SSO is not enabled, `401` when the IdP rejects the token or the email is unverified, and `400` when the token carries no email claim. Artalk does not provide a switch to bypass email verification.
::: tip
Validation relies entirely on the issuer's `/userinfo` rejecting invalid or revoked tokens, and the user is matched by the `email` claim. Only enable this for an issuer you trust. Because an unverified email could impersonate an existing user or administrator, Artalk requires `email_verified: true`.
:::
## Plugin Development
The social login feature of Artalk is implemented through an independent plugin developed using Solid.js. The code can be found in [@ArtalkJS/Artalk:ui/plugin-auth](https://github.com/ArtalkJS/Artalk/tree/master/ui/plugin-auth).
+314
View File
@@ -0,0 +1,314 @@
# Artalk v2.10.0 Release Notes and Migration Guide
v2.10.0 is the next stable release after v2.9.1. It adds page voting, SSO token exchange, MySQL Cloud SQL TLS, and CGO-free SQLite builds, while reorganizing the UI client's lifecycle, configuration, and type system.
::: warning Read before upgrading
v2.10.0 contains intentional API and behavior changes. Upgrade the server and UI client together. Themes, plugins, direct HTTP API consumers, and projects importing Artalk TypeScript types should complete the migrations below first.
:::
## Upgrade checklist
- Back up the database, configuration files, and custom frontend assets.
- For binary deployments, verify the working directory and every relative file path; pass `-w` and an absolute `-c` path to retain the previous behavior.
- Upgrade source-build and packaging environments to Go 1.26.5.
- To change the application timezone, set `timezone` or `ATK_TIMEZONE` and fully restart Artalk.
- Upgrade the server and UI client together to avoid mixing the old and new Vote APIs.
- Review local-versus-remote UI configuration priority; local configuration is now preferred by default.
- Search for `useBackendConf`, `remoteConfModifier`, two-argument `ctx.inject()`, assignments to `ctx.conf` / `ctx.$root`, and `artalk.update(...).xxx()` chains.
- Rebuild themes and plugins that import Artalk types or internal UI services.
- Use a modern Node.js toolchain; the Artalk repository development baseline is Node.js 22.19 or later.
## Highlights
### UI and interaction
- Added [page voting](../frontend/voting.md#page-voting) and the ability to query the current visitor's vote state ([#983](https://github.com/ArtalkJS/Artalk/issues/983), [#998](https://github.com/ArtalkJS/Artalk/issues/998)).
- Added `preferRemoteConf` to make local-versus-remote UI configuration precedence explicit ([#1000](https://github.com/ArtalkJS/Artalk/issues/1000), [#1011](https://github.com/ArtalkJS/Artalk/issues/1011)).
- Introduced a dependency injection container and reorganized UI modules, lifecycle handling, and public types ([#1006](https://github.com/ArtalkJS/Artalk/issues/1006), [#1007](https://github.com/ArtalkJS/Artalk/issues/1007)).
- Added the Artalk UI ESLint plugin and updated it to v1.0.2 ([#995](https://github.com/ArtalkJS/Artalk/issues/995), [#1010](https://github.com/ArtalkJS/Artalk/issues/1010)).
- Extended User-Agent detection for OpenHarmony and more browsers ([#1087](https://github.com/ArtalkJS/Artalk/issues/1087)).
- Added `ugc` to comment-link `rel` attributes ([#1120](https://github.com/ArtalkJS/Artalk/issues/1120)).
### Server and API
- Added `POST /api/v2/sso/exchange` to exchange an access token from a trusted OIDC identity provider for an Artalk session token ([#1134](https://github.com/ArtalkJS/Artalk/issues/1134)). It is disabled by default; configure only a trusted HTTPS issuer in production, and require `/userinfo` to return `email_verified: true`.
- Unified the Vote API around a target type, target ID, and choice, and added a vote-status endpoint.
- Improved configuration-file and data-directory discovery while preserving the located configuration before changing the data directory ([#992](https://github.com/ArtalkJS/Artalk/issues/992)).
- Added mutual TLS configuration for MySQL on Google Cloud SQL ([#1104](https://github.com/ArtalkJS/Artalk/issues/1104)).
### Builds and runtime
- SQLite now uses a pure-Go driver. Stable builds no longer require CGO or a separate `GORELEASER_CROSS` toolchain, so standard Go tooling can produce cross-platform artifacts directly.
- Upgraded the Go toolchain to v1.26.5 and updated related dependencies, including the maintained v5 JWT implementation used for login sessions. Existing HS256 token claims and signatures remain wire-compatible, so this server upgrade does not itself force users to sign in again.
- Updated the official container build and runtime bases to Alpine 3.24. The Go builder remains pinned to v1.26.5, and container data-directory and configuration paths are unchanged.
- Updated the frontend and documentation toolchain, including Vite 8.1, Marked 18.0.7, and KaTeX 0.17. The `@artalk/plugin-katex` peer range still accepts KaTeX 0.16.45 through 0.17.x, so existing themes are not forced to replace KaTeX as part of this upgrade.
- Added scripts for fetching the plugin registry and checking versions already published to npm.
- npm packages use modern conditional exports; legacy Node.js 10 module resolution is no longer supported.
### Documentation and localization
- Added Turkish (`tr-TR`) and a Traditional Chinese configuration template.
- Added a Japanese README.
- Redesigned the landing page, added translations, and improved responsive font sizing.
- Integrated Git Changelog into the documentation site.
## Security and reliability
- The backend JWT parser now uses `github.com/golang-jwt/jwt/v5`, explicitly allows only HS256, continues to validate expiration, and has regression coverage for legacy token format, signing algorithms, and expiration behavior.
- The Markdown HTML sanitizer now uses the maintained `DOMPurify` instead of the unpatched, ReDoS-affected `insane` parser. Artalk retains its previous tag, attribute, class, style, and URL-scheme allowlists and adds malicious-markup and ReDoS regression tests.
- The frontend lockfile pins patched transitive versions of `fast-uri`, `brace-expansion`, `js-yaml`, `undici`, Vite, and other build and development tooling; the complete workspace dependency audit reports no known vulnerabilities.
- Google Cloud SQL TLS now loads the server CA, client certificate, and client key together, verifies the server certificate and hostname, and requires TLS 1.2 or later. All three certificate paths must be configured together so partial TLS configuration cannot be silently ignored.
- The Vote API rejects malformed, non-positive, missing, or unknown targets. Vote records and target counters are updated in one database transaction and roll back together on failure.
- Plugin URLs are filtered by the server: relative and trusted URLs may be sent to the client, while untrusted absolute URLs are removed.
- Plugin-registry downloads require HTTPS and approved hosts and have bounded redirects, response size, and request time.
- The npm version check no longer interpolates package names into a shell command and reports network or parsing errors as failures.
- Process timezone initialization now happens once instead of mutating global timezone state during concurrent requests.
## Breaking changes and migration
### 1. Local UI configuration is preferred by default
In v2.9.x, remote configuration overrode values passed to `Artalk.init()`. v2.10.0 prefers local configuration by default:
```text
Artalk.init() local config > environment variables > Dashboard = configuration file
```
No additional option is needed for the new default:
```js
Artalk.init({
server: 'https://comments.example.com',
site: 'My Site',
locale: 'en', // the local value is retained
})
```
Explicitly enable remote-first behavior when required:
```js
Artalk.init({
server: 'https://comments.example.com',
site: 'My Site',
preferRemoteConf: true,
})
```
`useBackendConf` is deprecated and is always treated as `true`. Setting it to `false` no longer prevents the client from reading configuration that the backend must provide; it is not a replacement for `preferRemoteConf`.
### 2. `remoteConfModifier` has been removed
`remoteConfModifier` is no longer part of the public configuration. Migrate according to its purpose:
- Put fixed overrides directly in `Artalk.init()`; local values win by default.
- Use `preferRemoteConf: true` when Dashboard values should win.
- Use `fetchCommentsOnInit` to control the initial comment request.
- Listen for `created` and call `update()` for post-initialization changes.
```js
const artalk = Artalk.init({
server: 'https://comments.example.com',
site: 'My Site',
fetchCommentsOnInit: false,
})
artalk.on('created', () => {
artalk.update({ locale: 'en' })
artalk.reload()
})
```
There is no equivalent hook for arbitrarily modifying remote configuration before plugin initialization.
### 3. Configuration types distinguish resolved values from inputs
`Config` represents a fully resolved configuration. `ConfigPartial` is the recursively optional input accepted by `Artalk.init()`, `update()`, and plugin configuration updates.
```ts
import type { Config, ConfigPartial } from 'artalk'
function createComments(conf: ConfigPartial) {
return Artalk.init(conf)
}
function readResolvedConfig(conf: Config) {
console.log(conf.server, conf.pagination.pageSize)
}
```
Code that used `Partial<ArtalkConfig>` for initialization should use `ConfigPartial`. `ArtalkConfig` remains importable but now represents the complete configuration.
`ConfigPartial` preserves functions, DOM elements, `Date`, `RegExp`, and array-element semantics instead of incorrectly expanding those values into recursively partial object fields.
### 4. DI registration and resolution are separate
The old two-argument `ctx.inject(key, value)` registration form has been removed. Register with `provide()` and resolve with `inject()`:
```ts
// v2.9.x
ctx.inject('example', createExample())
const example = ctx.get('example')
// v2.10.0
ctx.provide('example', () => createExample())
const example = ctx.inject('example')
```
`provide()` supports dependency lists and `singleton` / `transient` lifecycles. Custom TypeScript service keys should extend `Services` through declaration merging.
### 5. `ctx.conf` and `ctx.$root` cannot be replaced
Both remain readable, but assigning a new object or element throws an error with migration guidance:
```ts
// Incorrect
ctx.conf = nextConf
ctx.$root = nextElement
// Correct
ctx.updateConf(nextConf)
const conf = ctx.getConf()
const root = ctx.getEl()
```
To change the root element, destroy the old instance and create a new instance with a different `el`.
### 6. `Artalk.update()` returns `void`
`update()` no longer returns the Artalk instance. Split chained calls:
```js
// v2.9.x
artalk.update({ pageKey: '/next' }).reload()
// v2.10.0
artalk.update({ pageKey: '/next' })
artalk.reload()
```
### 7. Plugin initialization and the `created` event
Built-in, local, and network plugins now initialize after local and remote configuration have been merged. Network plugins no longer observe a temporary configuration from only one source.
`created` fires after every plugin has initialized, followed by `mounted`. Plugins that depended on the old order should perform pre-initialization work in the plugin function and use a `created` listener for work requiring the complete plugin environment.
### 8. Vote HTTP API and client methods
Old vote endpoint:
```http
POST /api/v2/votes/comment_up/123
```
New endpoints:
```http
GET /api/v2/votes/comment/123
POST /api/v2/votes/comment/123/up
```
`target_name` must be `comment` or `page`; `choice` must be `up` or `down`.
The generated client methods changed accordingly:
```ts
await api.votes.getVote('comment', 123)
await api.votes.createVote('comment', 123, 'up', { name, email })
```
The old endpoint and `votes.vote()` method do not have compatibility adapters.
### 9. SQLite now uses a pure-Go driver
The server SQLite driver has changed from the CGO implementation based on `mattn/go-sqlite3` to the pure-Go implementation provided by `github.com/libtnb/sqlite`. Stable builds no longer require a local C compiler, `CGO_ENABLED=1`, or the cross-compilation toolchain selected through `GORELEASER_CROSS_VERSION`.
The SQLite on-disk format has not changed. Existing databases configured through `db.file` can be used directly, so standard deployments do not require a database configuration change. Back up the database before upgrading as usual; Artalk will run its normal schema migrations on the first v2.10.0 startup.
Projects that build Artalk from source or embed it as a Go module should note the following:
- C compiler, CGO, and Goreleaser Cross configuration used only for SQLite can be removed.
- Do not rely on `mattn/go-sqlite3`-specific build tags, DSN parameters, C APIs, or dynamic extension-loading behavior.
- Re-run migration, concurrent read/write, and backup/restore tests when using custom SQLite compile options, extensions, or native backup integrations.
This change does not affect deployments using MySQL, PostgreSQL, or MSSQL.
### 10. Default configuration and data directories follow XDG rules
Without `--workdir` / `-w`, Artalk no longer always uses the invocation directory as its working directory. v2.10.0 uses the following discovery order:
- Configuration files: the invocation directory, its `data` subdirectory, `$XDG_CONFIG_HOME/artalk`, then `/etc/artalk`.
- Data directory: keep the previous working directory when `./data` exists; otherwise use `$XDG_DATA_HOME/artalk`, then `/var/lib/artalk`, or create `$XDG_DATA_HOME/artalk` if neither exists.
The configuration file is located before an implicit data-directory change. Relative database, log, and upload paths in that configuration—as well as relative file arguments passed to commands such as `import`, `export`, and `gen`—are still resolved from the final working directory.
Existing deployments with a `./data` directory keep the previous behavior. The official Docker image also passes an explicit working directory and configuration path, so it is not affected.
To preserve the previous path semantics for a binary deployment, specify both paths explicitly:
```bash
artalk -w /srv/artalk -c /srv/artalk/artalk.yml server
```
Before upgrading, verify every relative path including `db.file`, `log.filename`, `img_upload.path`, IP-region databases, and keyword files to avoid creating an empty database or files in the newly selected data directory.
### 11. Source builds require Go 1.26.5
The minimum version in `go.mod` has increased from Go 1.22.7 to Go 1.26.5. This affects `go install`, source builds, distribution packaging, and downstream projects that include Artalk in a Go build.
Users of official binaries and Docker images do not need to install Go. Source-build environments must use Go 1.26.5. CI systems that disable automatic Go Toolchain downloads or operate offline must install the matching toolchain in advance.
### 12. The application timezone is fixed at process initialization
The `timezone` configuration and `ATK_TIMEZONE` environment variable remain available for an Artalk application timezone that is independent of the host system or container `TZ` setting:
```yaml
timezone: America/New_York
```
```bash
ATK_TIMEZONE=America/New_York artalk server
```
Timezone is process-global state. v2.10.0 sets it once during Artalk process initialization and does not support changing it dynamically at runtime. After changing it in the configuration file, environment, or Dashboard, fully restart the Artalk process or container; using Dashboard “Apply settings” alone is not sufficient to switch timezone.
### 13. `marked` 18 and modern package entry points
The default Artalk build continues to include Markdown support. ArtalkLite does not bundle `marked`; downstream consumers must provide a compatible `marked@^18.0.3`.
Artalk's use of `Marked`, `Renderer`, `parse()`, `setOptions()`, `use()`, and tokenizer extensions does not rely on v18-only APIs. Upstream parsing details changed between v14 and v18, however, so projects with custom renderers, tokenizers, or output snapshots should rerun their Markdown tests.
Node.js 10 and toolchains that rely on legacy package resolution are no longer supported. Artalk repository development and builds use Node.js 22.19 or later.
## Retained compatibility entry points
The following entry points remain in v2.10.0 to give themes and third-party plugins time to migrate, but are marked `deprecated`:
| Previous entry point | Preferred entry point |
| --- | --- |
| `ContextApi` | `Context` |
| `DataManagerApi` | `DataManager` |
| `EditorApi` | `Editor` |
| `ctx.data` | `ctx.getData()` or `ctx.inject('data')` |
| `ctx.user` | `ctx.getUser()` or `ctx.inject('user')` |
| `ctx.editor` | `ctx.inject('editor')` |
| `ctx.list` | `ctx.inject('list')` |
| `ctx.layerManager` | `ctx.inject('layers')` |
| `ctx.checkerLauncher` | `ctx.inject('checkers')` |
| `ctx.sidebarLayer` | `ctx.inject('sidebar')` |
| `ctx.editorPlugs` | `ctx.inject('editorPlugs')` |
| `editor.$el` | `editor.getEl()` |
| `editor.getPlugs()` | `editor.getPlugins()` |
| `editor.setReply()` | `editor.setReplyComment()` |
These aliases are transitional. New code should use the preferred entry points directly. See [TypeDoc](https://artalk.js.org/typedoc/) for the complete public type surface.
## Other changes
- The comment reply button now uses a component implementation.
- Sidebar and Auth UI have been updated for the new client types.
- Added npm-published-version checking and plugin-registry generation workflows.
- Pinned the Swagger TypeScript client generator and updated its command so development environments do not receive incompatible versions through `npx`.
- Updated the GitHub Actions runtimes used for artifacts, caches, Docker, CodeQL, and Codecov while preserving workflow inputs and release-artifact layout.
- Updated frontend, Go, and documentation dependencies and removed obsolete test projects and CI configuration.
See the complete commit comparison at [`v2.9.1...v2.10.0`](https://github.com/ArtalkJS/Artalk/compare/v2.9.1...v2.10.0).
+6 -3
View File
@@ -136,13 +136,16 @@ Go to the [Disqus backend](https://disqus.com/admin), find "Moderation - Export"
### Commento
You can export the data file in JSON format from the Commento backend, then use the [conversion tool](#conversion-tool) for conversion.
Go to the Commento backend, click on "General - Export" in sequence, export the data file in JSON format, and then use the [conversion tool](#conversion-tool) for conversion.
[Image, to be supplemented...]
![](/images/transfer/commento.png)
### Twikoo
[Twikoo](https://twikoo.js.org/) can obtain the JSON file through the "Export" section in the administrative panel where it is located, and then convert it using the [conversion tool](#conversion-tool).
[Twikoo](https://twikoo.js.org/) is a comment system developed based on Tencent Cloud. Go to the [Tencent Cloud backend](https://console.cloud.tencent.com/tcb) to export the comment data file in JSON format, then use the [conversion tool](#conversion-tool) for conversion.
![](/images/transfer/twikoo-control.png)
In addition, for Twikoo tencent cloud deployment, you can travel to [Tencent Cloud Background](https://console.cloud.tencent.com/tcb) export comment JSON format data.
<img src="/images/transfer/tencent-tcb.png" style="max-width: 480px;">
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+7 -7
View File
@@ -15,13 +15,13 @@
"serve:docs": "vitepress serve"
},
"devDependencies": {
"@artalk/plugin-katex": "^0.2.4",
"@artalk/plugin-lightbox": "^0.2.4",
"@nolebase/vitepress-plugin-git-changelog": "^2.6.1",
"@artalk/plugin-katex": "workspace:^",
"@artalk/plugin-lightbox": "workspace:^",
"@nolebase/vitepress-plugin-git-changelog": "^2.18.2",
"artalk": "workspace:^",
"katex": "^0.16.11",
"lightgallery": "^2.7.2",
"vitepress": "1.4.1",
"vue": "^3.5.12"
"katex": "^0.17.0",
"lightgallery": "^2.9.0",
"vitepress": "1.6.4",
"vue": "^3.5.40"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"baseUrl": ".",
"composite": true,
"types": ["vite/client"],
"resolveJsonModule": true,
"module": "ESNext",
+4 -4
View File
@@ -24,9 +24,9 @@ cd Artalk
为了开发 Artalk 的前端和后端,请安装以下工具:
- [Node.js](https://nodejs.org/en/) (>= 20.17.0)
- [PNPM](https://pnpm.io/) (>= 9.10.0)
- [Go](https://golang.org/) (>= 1.22)
- [Node.js](https://nodejs.org/en/) (>= 22.19.0)
- [PNPM](https://pnpm.io/) (>= 10.33.2)
- [Go](https://golang.org/) (>= 1.26)
- [Docker](https://www.docker.com/) (>= 20.10.0)(可选)
- [Docker Compose](https://docs.docker.com/compose/) (>= 1.29.0)(可选)
@@ -224,7 +224,7 @@ docker build -t artalk:TAG .
如果你已经在 Docker 容器外部构建了前端,可以跳过容器内的前端构建过程以加快构建速度。使用以下命令:
```sh
docker build --build-arg SKIP_FRONTEND_BUILD=true -t artalk:latest .
docker build --build-arg SKIP_UI_BUILD=true -t artalk:latest .
```
更多详细信息,请参考 `Dockerfile`。
+2 -2
View File
@@ -105,7 +105,7 @@ import Artalk from 'artalk'
const ArtalkComment = () => {
const { pathname } = useLocation()
const artalk = useRef<Artalk>()
const artalk = useRef<Artalk | null>(null)
const handleContainerInit = useCallback(
(node: HTMLDivElement | null) => {
@@ -114,7 +114,7 @@ const ArtalkComment = () => {
}
if (artalk.current) {
artalk.current.destroy()
artalk.current = undefined
artalk.current = null
}
artalk.current = Artalk.init({
el: node,
@@ -0,0 +1,347 @@
# 2026-05 依赖升级说明
本文档记录了本次大规模依赖升级所涉及的全部变更,涵盖前端工具链、ESLint 生态、TypeScript 编译配置、构建工具迁移及相关文档与 CI 的同步更新。
---
## 一、前端依赖升级
### 核心工具链
| 包 | 旧版本 | 新版本 |
|----|--------|--------|
| `typescript` | 5.6.3 | 6.0.3 |
| `vite` | 5.4.9 | 8.0.10 |
| `vitest` | ^2.1.3 | ^4.1.5 |
| `rollup` | 4.24.0 | 4.60.3 |
| `sass` | 1.80.3 | 1.99.0 |
| `terser` | 5.36.0 | 5.46.2 |
| `postcss` | 8.4.47 | 8.5.14 |
| `autoprefixer` | 10.4.20 | 10.5.0 |
| `tsx` | ^4.19.1 | ^4.21.0 |
| `cross-env` | ^7.0.3 | ^10.1.0 |
| `prettier` | 3.3.3 | 3.8.3 |
### ESLint 生态
| 包 | 旧版本 | 新版本 |
|----|--------|--------|
| `eslint` | ^9.13.0 | ^10.3.0 |
| `typescript-eslint` | ^8.10.0 | ^8.59.1 |
| `@typescript-eslint/eslint-plugin` | 8.10.0 | 8.59.1 |
| `eslint-plugin-vue` | ^9.29.0 | ^10.9.0 |
| `eslint-plugin-import-x` | ^4.3.1 | ^4.16.2 |
| `eslint-plugin-compat` | ^6.0.1 | ^7.0.2 |
| `eslint-plugin-react-hooks` | 5.1.0-beta | 7.1.1 |
| `eslint-config-prettier` | 9.1.0 | 10.1.8 |
| `eslint-import-resolver-typescript` | 3.6.3 | 4.4.4 |
| `vue-eslint-parser` | ^9.4.3 | ^10.4.0 |
| `globals` | ^15.11.0 | ^17.6.0 |
### Vite 插件
| 包 | 旧版本 | 新版本 |
|----|--------|--------|
| `vite-plugin-dts` | 4.2.4 | 5.0.0 |
| `vite-plugin-checker` | 0.8.0 | 0.13.0 |
### UI 子包
| 包 | 子包 | 旧版本 | 新版本 |
|----|------|--------|--------|
| `marked` | `ui/artalk` (dep + peer) | ^14.1.3 | ^18.0.3 |
| `vue` | artalk-sidebar, docs | ^3.5.12 | ^3.5.33 |
| `vue-router` | artalk-sidebar | ^4.4.5 | ^5.0.6 |
| `vue-i18n` | artalk-sidebar | ^10.0.4 | ^11.4.0 |
| `pinia` | artalk-sidebar | ^2.2.4 | ^3.0.4 |
| `vue-tsc` | artalk-sidebar | ^2.1.6 | ^3.2.8 |
| `@vitejs/plugin-vue` | artalk-sidebar | ^5.1.4 | ^6.0.6 |
| `unplugin-auto-import` | artalk-sidebar | ^0.18.3 | ^21.0.0 |
| `unplugin-vue-components` | artalk-sidebar | ^0.27.4 | ^32.0.0 |
| `react` / `react-dom` | docs/landing | ^18.3.1 | ^19.2.5 |
| `@vitejs/plugin-react-swc` | docs/landing | ^3.7.1 | ^4.3.0 |
| `react-i18next` | docs/landing | ^15.0.3 | ^17.0.6 |
| `i18next` | docs/landing | ^23.16.0 | ^26.0.8 |
| `katex` | plugin-katex | ^0.16.11 | ^0.16.45 |
| `lightgallery` | plugin-lightbox | ^2.7.2 | ^2.9.0 |
| `vitepress` | docs/docs | 1.4.1 | 1.6.4 |
| `@redocly/cli` | docs/swagger | 1.25.7 | 2.30.3 |
| `solid-js` | plugin-auth | ^1.9.2 | ^1.9.12 |
---
## 二、包管理器与运行时要求更新
**`package.json` 根配置:**
```jsonc
// 旧
"packageManager": "pnpm@9.10.0"
// 新
"packageManager": "pnpm@10.33.2",
"engines": {
"node": ">=22.19.0",
"pnpm": ">=10.33.2"
}
```
新增 `pnpm.peerDependencyRules.allowedVersions` 以抑制 peer dep 版本警告:
```json
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"eslint-plugin-import>eslint": "10",
"eslint-plugin-react>eslint": "10",
"artalk>marked": "18"
}
}
}
```
新增 `prepare` 脚本,`pnpm install` 后自动构建本地 workspace 包:
```json
"prepare": "pnpm build:plugin-kit && pnpm build:eslint-plugin"
```
本地 workspace 引用方式也由 npm 发布版改为 workspace 直引:
```jsonc
// 旧
"@artalk/plugin-kit": "^1.0.7",
"eslint-plugin-artalk": "^1.0.2",
// 新
"@artalk/plugin-kit": "workspace:*",
"eslint-plugin-artalk": "workspace:*",
```
---
## 三、构建工具迁移:tsup → tsdown
`eslint-plugin-artalk` 和 `@artalk/plugin-kit` 从 `tsup`(基于 esbuild)迁移到 `tsdown`(基于 rolldown)。
### 迁移对比
| 项目 | tsup | tsdown |
|------|------|--------|
| 构建器内核 | esbuild | rolldown |
| `?raw` 文件插件 | `esbuild-plugin-raw`,API:`setup/onResolve/onLoad` | 内联 rolldown 插件,API:`resolveId/load` |
| externals | `external: [...]` | `deps.neverBundle: [...]` |
| 产物扩展名 | `.js` / `.d.ts` | `.mjs`/`.cjs` / `.d.mts`/`.d.cts` |
| 插件类型 | `esbuild.Plugin` | `TsdownPlugin`(从 `tsdown` 直接导入) |
### 产物入口更新
由于产物扩展名变化,`package.json` 的 `main`/`types`/`exports` 字段同步更新:
```jsonc
// 旧
"main": "dist/main.js",
"types": "dist/main.d.ts",
"exports": {
"require": { "types": "./dist/main.d.cjs" },
"default": { "types": "./dist/main.d.ts", "default": "./dist/main.js" }
}
// 新
"main": "dist/main.cjs",
"types": "dist/main.d.cts",
"exports": {
"require": { "types": "./dist/main.d.cts", "default": "./dist/main.cjs" },
"default": { "types": "./dist/main.d.mts", "default": "./dist/main.mjs" }
}
```
---
## 四、eslint-plugin-artalk 重构
### 问题背景
`eslint@10` 内置 `@eslint/core@1.2.1`,而 `typescript-eslint@8` 依赖 `@eslint/core@0.7.0`,两者的 `RuleContext` API 不兼容,导致 `TSESLint.RuleModule` 无法直接赋值给 `eslint` 的 `Rule.RuleModule`。
### 解决方案
完全弃用 `ESLintUtils.RuleCreator`,改为直接使用 `eslint` 原生的 `Rule.RuleModule` 类型构造规则,无需类型桥接或强制转换。
**`src/helper.ts`(重写):**
```ts
import type { Rule } from 'eslint'
export function createRule(def: {
name: string
meta: Rule.RuleMetaData
defaultOptions?: unknown[]
create: Rule.RuleModule['create']
}): Rule.RuleModule {
return {
meta: { ...def.meta, docs: { ...def.meta.docs, url: `${docsBaseUrl}${def.name}` } },
create: def.create,
}
}
```
**`src/main.ts`(重写):**
改为 default-only 导出,避免 rolldown MIXED_EXPORTS 警告:
```ts
import type { ESLint } from 'eslint'
const plugin: ESLint.Plugin & { configs: typeof configs } = {
rules: { 'artalk-plugin': artalkPlugin },
configs,
}
export default plugin
```
---
## 五、TypeScript 编译配置重构
### `tsconfig.base.json`
- 新增 `"composite": true`:支持项目引用(Project References),使 ESLint `projectService` 能正确解析各子包
- 移除 `"noEmit": true`(与 `composite` 不兼容)
- 移除 `"experimentalDecorators": true`(TypeScript 5+ 已默认支持)
- 移除 `"allowSyntheticDefaultImports": true`(`esModuleInterop: true` 已隐含)
### 新增根级 `tsconfig.json`
新增仅供工具链(ESLint、IDE)使用的根 tsconfig,通过 `references` 聚合所有子包:
```json
{
"extends": "./tsconfig.base.json",
"compilerOptions": { "noEmit": true, "allowJs": true },
"files": ["eslint.config.mjs", "vitest.workspace.ts", ...],
"references": [
{ "path": "./ui/artalk" },
{ "path": "./ui/artalk-sidebar" },
...
]
}
```
### 各子包 `tsconfig.json` 变更
需要 composite 支持但由 vite 构建(不 emit)的包(`artalk-sidebar`、`docs/landing`):
```jsonc
// 不能用 noEmit,改为
"emitDeclarationOnly": true,
"declarationDir": ".tsbuildinfo-dts"
```
并将 `.tsbuildinfo-dts/` 加入 `.gitignore`。
`ui/artalk/tsconfig.json` 新增:
```json
"include": ["./src", "./types", "./tests", "./scripts", "package.json"],
"files": ["./package.json"]
```
原因:`composite: true` 下所有 import 的文件必须被 tsconfig 显式涵盖;`import { version } from '~/package.json'` 的 JSON 文件不会被目录 glob 匹配,需手动列出。同时 `vite-plugin-dts` 检查 `include` 而 `tsc` 检查 `files`,因此两处均需声明。
`ui/artalk/vite.config.ts` 为 `vite-plugin-dts` 添加:
```ts
compilerOptions: { composite: false }
```
避免 dts 插件在生成声明文件时触发 composite 文件列表检查(TS6307)。
---
## 六、ESLint 配置重构(`eslint.config.mjs`)
| 变更 | 旧 | 新 |
|------|----|----|
| 配置入口 | `eslintTs.config()` | `defineConfig()` from `'eslint/config'` |
| typescript-eslint 配置展开 | `...eslintTs.configs.recommended` | `eslintTs.configs.recommended`(无需展开) |
| vue 配置展开 | `...pluginVue.configs['flat/recommended']` | `pluginVue.configs['flat/recommended']` |
| `@typescript-eslint` 插件 | 手动注册 `pluginTS` | 由 `typescript-eslint` 内部管理,移除手动注册 |
| TypeScript 项目解析 | `project: tsProjects`(数组 glob) | `projectService: true`(语言服务模式) |
| import-x resolver | `project: tsProjects` | `project: './tsconfig.json'`(单一根 tsconfig) |
| React 版本 | `version: '18.3'` | `version: '19.2'` |
| typed linting | `recommended` | `recommendedTypeChecked` |
---
## 七、依赖分类修复
### `ui/plugin-katex`
- `katex`:从 `dependencies` 移至 `devDependencies`(vite 配置中已 external,不打包进产物,应由用户提供)
- `@types/katex`:从 `dependencies` + `peerDependencies` 完全移除(katex >= 0.16 已自带 `types/katex.d.ts`,`@types/katex` 为旧版遗留包)
### `ui/eslint-plugin-artalk`
- 新增 `eslint: "^10.3.0"` 到 `devDependencies`(peerDep 已声明但开发依赖缺失)
### `ui/plugin-kit`
- 新增 `vite: "^8.0.10"` 到 `devDependencies`(源码中 `import type { ViteDevServer, Plugin } from 'vite'`,peerDep 已声明但开发依赖缺失)
### `ui/plugin-auth`、`ui/plugin-katex`、`ui/plugin-lightbox`
- 新增 `"@artalk/plugin-kit": "workspace:^"` 到各自 `devDependencies`(这些插件基于 plugin-kit 开发,之前遗漏了显式声明)
### 根 `package.json`
- `@microsoft/api-extractor` 保留在根级(由 `vite-plugin-dts` 的 `bundleTypes: true` 调用,属于根级工具链依赖)
---
## 八、vitest 配置迁移
`vitest.workspace.ts` 从 `defineWorkspace` API 迁移到 `defineConfig` + `test.projects`(vitest v4 新 API):
```ts
// 旧
import { defineWorkspace } from 'vitest/config'
export default defineWorkspace(['./ui/artalk/vitest.config.ts'])
// 新
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: { projects: ['./ui/artalk/vitest.config.ts'] },
})
```
---
## 九、GitHub Actions CI 更新
所有使用 Node.js 和 pnpm 的工作流同步更新版本:
| 文件 | Node.js | pnpm |
|------|---------|------|
| `test-frontend.yml` | `[18, 20]` → `[22]` | `9.10.0` → `10.33.2` |
| `build-ui.yml` | `20.x` → `22.x` | `9.10.0` → `10.33.2` |
| `test-docs.yml` | `20.x` → `22.x` | `9.10.0` → `10.33.2` |
| `docs-cn.yml` | `20` → `22` | `9.10.0` → `10.33.2` |
| `build-nightly.yml` | `20.x` → `22.x` | `9.10.0` → `10.33.2` |
| `release.yml` | `20.x` → `22.x` | — |
---
## 十、文档更新
- `CONTRIBUTING.md`(英文):Node.js 要求 `>=20.17.0` → `>=22.19.0`,pnpm `>=9.10.0` → `>=10.33.2`,修复 `make build-fronted` 拼写错误为 `make build-frontend`
- `docs/docs/zh/develop/contributing.md`(中文):同步上述版本要求更新
- `docs/swagger/redocly.yaml`:更新 Redocly 配置适配 CLI v2 的新格式
- `docs/swagger/package.json`:`swagger:serve` 命令 `npx @redocly/cli preview-docs` → `redocly preview`
---
## 十一、Go 后端依赖更新
`go.mod` / `go.sum` 同步更新了若干后端 Go 模块依赖(具体版本见 `go.mod` diff)。
+10
View File
@@ -283,6 +283,16 @@ timezone: Asia/Shanghai
该值填写你所在地时区,对应 IANA 数据库时区名,参考:[Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) / [RFC-6557](https://www.rfc-editor.org/rfc/rfc6557.html)。
`timezone` 设置的是 Artalk 应用使用的时区,可以与宿主系统或容器的 `TZ` 时区不同。也可以使用环境变量 `ATK_TIMEZONE` 设置相同配置:
```bash
ATK_TIMEZONE=Asia/Shanghai artalk server
```
::: warning 修改后需要重启
应用时区是进程级固定值,只在 Artalk 进程初始化时设置。运行期间不支持动态切换;修改配置文件、环境变量或控制中心中的时区后,必须完整重启 Artalk 进程或容器才能生效。
:::
```
UTC+08:00 Asia/Shanghai
UTC+09:00 Asia/Tokyo
+1
View File
@@ -109,6 +109,7 @@ moderator:
- **pending**:当成功匹配时,是否将评论设为待审核状态。
- **files**:词库文件。允许多个文件,Artalk 启动时会合并词库。
- **file_sep**:词库文件内容分割符。例如:文件中每行一个词语,该项配置 `\n`。
该配置不能为空。控制中心会将换行、回车和制表符分别显示为 `\n`、`\r`、`\t`;若分隔符需要是字面量 `\n`,请输入 `\\n`。
- **replace_to**:替换字符。例如:该项设置为 `x`,你可以将 `pending` 设置为 `false`,评论自动过审,但匹配到的词语会被替换为 `x`,例如 `fxxk`、`xxxx`。
注:`replace_to` 不建议使用 `*` 星号,应为它和 Markdown 的加粗语法冲突。
+4 -1
View File
@@ -2,7 +2,10 @@
## 更新变动
突破性变动,例如更新需要修改原有配置等,请注意查看版本更新日志中的提示:[CHANGELOG.md](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md)
突破性变动,例如更新需要修改原有配置等,请注意查看版本更新日志中的提示:
- [v2.10.0 变更日志与迁移指南](../releases/v2.10.0.md)
- [完整 CHANGELOG.md](https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md)
## 命令行一键升级
+1
View File
@@ -36,6 +36,7 @@
- [青空之蓝](https://blog.ixk.me/)
- [Monstx](https://blog.monsterx.cn/)
- [QWQAQ](https://qwqaq.com/)
- [xaoxuu](https://xaoxuu.com)
::: tip
+5
View File
@@ -25,6 +25,7 @@ services:
- ./data:/data
environment:
- TZ=Asia/Shanghai
- ATK_TIMEZONE=Asia/Shanghai
- ATK_LOCALE=zh-CN
- ATK_SITE_DEFAULT=Artalk 的博客
- ATK_SITE_URL=https://example.com
@@ -36,6 +37,8 @@ services:
- ATK_ADMIN_USERS_0_BADGE_COLOR=#0083FF
```
`TZ` 设置容器或宿主进程环境的时区,`ATK_TIMEZONE` 设置 Artalk 应用时区;两者可以不同。Artalk 应用时区只在进程初始化时生效,修改 `ATK_TIMEZONE` 后必须完整重启 Artalk 进程或容器。
当变量为数组,通过空格分隔的字符串或数字索引来设置数组值,例如:
```
@@ -165,6 +168,8 @@ ATK_TRUSTED_DOMAINS_0="https://a.com"
| **ATK_AUTH_SLACK_CLIENT_ID** | `""` | ClientId | auth.slack.client_id (社交登录 > Slack > ClientId) |
| **ATK_AUTH_SLACK_CLIENT_SECRET** | `""` | ClientSecret | auth.slack.client_secret (社交登录 > Slack > ClientSecret) |
| **ATK_AUTH_SLACK_ENABLED** | `false` | 启用 | auth.slack.enabled (社交登录 > Slack > Enabled) |
| **ATK_AUTH_SSO_ENABLED** | `false` | 启用 SSO 令牌交换 | auth.sso.enabled (社交登录 > SSO > 启用 SSO 令牌交换) |
| **ATK_AUTH_SSO_ISSUER** | `""` | OIDC issuer 地址 (如 "tenant.auth0.com" 或 "https://tenant.auth0.com") | auth.sso.issuer (社交登录 > SSO > OIDC issuer 地址) |
| **ATK_AUTH_STEAM_API_KEY** | `""` | ApiKey | auth.steam.api_key (社交登录 > Steam > ApiKey) |
| **ATK_AUTH_STEAM_ENABLED** | `false` | 启用 | auth.steam.enabled (社交登录 > Steam > Enabled) |
| **ATK_AUTH_TIKTOK_CLIENT_ID** | `""` | ClientId | auth.tiktok.client_id (社交登录 > Tiktok > ClientId) |
+56
View File
@@ -69,6 +69,62 @@ Artalk 支持同时启用多种登录方式,用户可以选择任意一种方
接入 GitHub 登录可参考文档:[关于创建 GitHub 应用](https://docs.github.com/zh/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps),得到 Client ID 和 Client Secret 后,填写到 Artalk 控制中心的设置页面的社交登录中的「GitHub」选项中即可。
## SSO 令牌交换
如果你的站点被嵌入在一个已经通过外部 OIDC 身份提供商(Auth0、Keycloak、Okta 等)完成用户认证的应用中,可以让 Artalk 复用该登录态,而不必显示自己的登录界面。外层应用用它已经持有的 IdP 访问令牌换取一个 Artalk 会话令牌,这样已在父应用登录的用户无需额外点击或弹窗即可发表评论。
该功能**默认关闭**,需手动开启。可通过 [配置文件](../backend/config.md) 或 [环境变量](../env.md#社交登录) 启用,并填写你所用提供商的 OIDC issuer:
```yaml
auth:
enabled: true
sso:
enabled: true
issuer: "https://tenant.auth0.com" # 例如 "tenant.auth0.com" 或 "https://tenant.auth0.com"
```
启用后,外层应用将 IdP 访问令牌提交到交换接口:
```
POST /api/v2/sso/exchange
Content-Type: application/json
{ "token": "<外部 IdP 访问令牌>" }
```
Artalk 通过调用 issuer 的 `/userinfo` 端点校验该令牌(OIDC 标准做法——由 IdP 在服务端验证并签名响应,因此 Artalk 侧无需处理密钥),读取其中的 `email` 声明。只有同时返回 `email_verified: true` 时,Artalk 才会查找或创建对应用户,并返回与其它登录接口一致的响应结构:
```json
{
"token": "<Artalk 会话令牌>",
"user": { "name": "...", "email": "...", "is_admin": false }
}
```
前端在 `Artalk.init()` 运行前将其写入 `localStorage["ArtalkUser"]`,组件即把该用户视为已完全登录(不弹窗,管理员用户也不再要求输入管理密码):
```js
const res = await fetch('https://comments.example.com/api/v2/sso/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: idpAccessToken }), // 来自你的 IdP 的访问令牌
})
const { token, user } = await res.json()
// 在组件挂载前预填 Artalk 登录态
localStorage.setItem('ArtalkUser', JSON.stringify({ ...user, token }))
Artalk.init({ el: '#Comments', server: 'https://comments.example.com', site: 'Blog' })
```
当 SSO 未启用时接口返回 `404`,IdP 拒绝令牌或邮箱未验证时返回 `401`,令牌不含邮箱声明时返回 `400`。Artalk 不提供跳过邮箱验证的开关。
::: tip
校验完全依赖 issuer 的 `/userinfo` 拒绝无效或已吊销的令牌,且用户按 `email` 声明匹配。请仅对你信任的 issuer 启用此功能。未经验证的邮箱可能冒充现有用户甚至管理员,因此 Artalk 强制要求 `email_verified: true`。
:::
## 插件开发
Artalk 的社交登录功能是通过独立的插件实现并采用 Solid.js 开发,代码可在 [@ArtalkJS/Artalk:ui/plugin-auth](https://github.com/ArtalkJS/Artalk/tree/master/ui/plugin-auth) 找到。
+314
View File
@@ -0,0 +1,314 @@
# Artalk v2.10.0 变更日志与迁移指南
v2.10.0 是 v2.9.1 之后的下一个正式版本。本版本新增页面投票、SSO Token Exchange、MySQL Cloud SQL TLS 和无 CGO 的 SQLite 构建,并重构了 UI 客户端的生命周期、配置与类型系统。
::: warning 升级前必读
v2.10.0 包含有意保留的接口与行为变更。服务端与 UI 客户端应同步升级;主题、插件、直接调用 HTTP API 或引用 Artalk TypeScript 类型的项目应先按本文完成迁移。
:::
## 升级检查清单
- 备份数据库、配置文件和自定义前端资源。
- 二进制部署确认工作目录和所有相对文件路径;需要保持旧行为时显式传入 `-w` 和绝对 `-c` 路径。
- 自行编译或打包服务端时升级到 Go 1.26.5。
- 如需修改应用时区,设置 `timezone` 或 `ATK_TIMEZONE` 后完整重启 Artalk。
- 同时升级服务端与 UI 客户端,避免新旧 Vote API 混用。
- 检查本地与远程界面配置的优先级;默认改为本地配置优先。
- 搜索 `useBackendConf`、`remoteConfModifier`、两参数 `ctx.inject()`、直接赋值 `ctx.conf` / `ctx.$root` 和 `artalk.update(...).xxx()`。
- 重新编译使用 Artalk 类型或内部 UI 服务的主题与插件。
- Node.js 工具链使用现代版本;Artalk 仓库的开发基线为 Node.js 22.19 及以上。
## 主要更新
### UI 与交互
- 新增[页面投票](../frontend/voting.md#页面投票),并可显示当前访客的已投票状态([#983](https://github.com/ArtalkJS/Artalk/issues/983)、[#998](https://github.com/ArtalkJS/Artalk/issues/998))。
- 新增 `preferRemoteConf`,明确本地与远程界面配置的合并优先级([#1000](https://github.com/ArtalkJS/Artalk/issues/1000)、[#1011](https://github.com/ArtalkJS/Artalk/issues/1011))。
- UI 改用依赖注入容器,并重新整理模块、生命周期和公开类型([#1006](https://github.com/ArtalkJS/Artalk/issues/1006)、[#1007](https://github.com/ArtalkJS/Artalk/issues/1007))。
- 新增 Artalk UI ESLint 插件,并升级到 v1.0.2([#995](https://github.com/ArtalkJS/Artalk/issues/995)、[#1010](https://github.com/ArtalkJS/Artalk/issues/1010))。
- User-Agent 识别新增 OpenHarmony 和更多浏览器支持([#1087](https://github.com/ArtalkJS/Artalk/issues/1087))。
- 评论链接的 `rel` 属性新增 `ugc`([#1120](https://github.com/ArtalkJS/Artalk/issues/1120))。
### 服务端与 API
- 新增 `POST /api/v2/sso/exchange`,可将受信任 OIDC 身份提供方的访问令牌交换为 Artalk 会话令牌([#1134](https://github.com/ArtalkJS/Artalk/issues/1134))。该功能默认关闭;生产环境应只配置受信任的 HTTPS Issuer,且 `/userinfo` 必须返回 `email_verified: true`。
- Vote API 现在统一使用目标类型、目标 ID 和选择,并新增投票状态查询接口。
- 改进配置文件与数据目录查找,保留切换数据目录前的配置文件定位结果([#992](https://github.com/ArtalkJS/Artalk/issues/992))。
- MySQL 新增 Google Cloud SQL 双向 TLS 配置支持([#1104](https://github.com/ArtalkJS/Artalk/issues/1104))。
### 构建与运行环境
- SQLite 改用纯 Go 驱动,正式构建不再依赖 CGO,也不再需要单独的 `GORELEASER_CROSS` 工具链。跨平台产物可由标准 Go 工具链直接构建。
- Go 工具链升级到 v1.26.5,并更新相关依赖;登录会话的 JWT 实现升级到受维护的 v5 版本。已有 HS256 Token 的声明与签名格式保持兼容,升级服务端不会因此强制用户重新登录。
- 官方容器的构建与运行基础镜像升级到 Alpine 3.24;Go 构建版本仍固定为 v1.26.5,容器内的数据目录和配置路径保持不变。
- 更新前端与文档工具链,包括 Vite 8.1、Marked 18.0.7 和 KaTeX 0.17。`@artalk/plugin-katex` 的 peer 范围仍兼容 KaTeX 0.16.45 至 0.17.x,依赖升级不会强制现有主题同步更换 KaTeX。
- 新增插件注册表拉取脚本和 npm 已发布版本检查脚本。
- npm 包使用现代条件导出;不再兼容 Node.js 10 的旧模块解析行为。
### 文档与本地化
- 新增土耳其语(`tr-TR`)和繁体中文配置模板。
- 新增日语 README。
- 重设计项目首页,补充多语言内容并优化响应式字号。
- 文档站集成 Git Changelog。
## 安全性与可靠性
- 后端 JWT 解析器升级到 `github.com/golang-jwt/jwt/v5`,显式限制为 HS256,继续校验过期时间,并新增旧 Token 格式、签名算法和过期行为的回归测试。
- Markdown HTML 清理器由无修复版本且存在 ReDoS 风险的 `insane` 切换为持续维护的 `DOMPurify`,保留原有标签、属性、class、style 和 URL scheme 白名单,并增加恶意标记与 ReDoS 回归测试。
- 前端锁文件将构建与开发工具链中的 `fast-uri`、`brace-expansion`、`js-yaml`、`undici`、Vite 等传递依赖固定到已修复版本;完整工作区依赖审计未发现已知漏洞。
- Google Cloud SQL TLS 会同时加载服务端 CA、客户端证书与私钥,校验服务器证书和主机名,并要求 TLS 1.2 及以上。三个证书路径必须一起配置,避免部分配置被静默忽略。
- Vote API 会拒绝非法、非正数、不存在或类型未知的投票目标。投票记录和目标计数在同一数据库事务中更新,任一步骤失败都会回滚。
- 插件 URL 由服务端过滤:相对 URL 与受信任 URL 可下发给客户端,不受信任的绝对 URL 会被移除。
- 插件注册表下载仅允许 HTTPS 和限定主机,并限制重定向次数、响应大小与请求时间。
- npm 版本检查不再通过 Shell 拼接包名,并会把网络或解析错误作为失败返回。
- 进程时区只在初始化时设置,避免并发请求修改全局时区状态。
## 突破性变更与迁移
### 1. 本地界面配置默认优先
v2.9.x 中远程配置会覆盖 `Artalk.init()` 传入的本地配置。v2.10.0 默认改为本地配置优先:
```text
Artalk.init() 本地配置 > 环境变量 > 控制中心 = 配置文件
```
默认无需新增配置:
```js
Artalk.init({
server: 'https://comments.example.com',
site: 'My Site',
locale: 'zh-CN', // 保留本地值
})
```
如需恢复“远程配置优先”,显式开启:
```js
Artalk.init({
server: 'https://comments.example.com',
site: 'My Site',
preferRemoteConf: true,
})
```
`useBackendConf` 已废弃并始终按 `true` 处理。设置为 `false` 不再阻止客户端读取后端必须提供的配置;它不能替代 `preferRemoteConf`。
### 2. 删除 `remoteConfModifier`
`remoteConfModifier` 不再属于公开配置。请根据用途迁移:
- 固定覆盖项直接放入 `Artalk.init()`,默认会优先于远程配置。
- 需要控制中心优先时使用 `preferRemoteConf: true`。
- 需要控制首次评论请求时使用 `fetchCommentsOnInit`。
- 仅需初始化后更新时,监听 `created` 后调用 `update()`。
```js
const artalk = Artalk.init({
server: 'https://comments.example.com',
site: 'My Site',
fetchCommentsOnInit: false,
})
artalk.on('created', () => {
artalk.update({ locale: 'zh-CN' })
artalk.reload()
})
```
不再提供可在插件初始化前任意修改远程配置的等价 Hook。
### 3. 配置类型区分完整值与输入值
`Config` 表示已经解析完成的完整配置,`ConfigPartial` 表示 `Artalk.init()`、`update()` 和插件配置更新所接受的递归可选输入。
```ts
import type { Config, ConfigPartial } from 'artalk'
function createComments(conf: ConfigPartial) {
return Artalk.init(conf)
}
function readResolvedConfig(conf: Config) {
console.log(conf.server, conf.pagination.pageSize)
}
```
过去将 `Partial<ArtalkConfig>` 用作初始化参数的代码应改用 `ConfigPartial`。`ArtalkConfig` 仍可导入,但现在表示完整配置。
`ConfigPartial` 会保持函数、DOM 元素、`Date`、`RegExp` 和数组元素的原始语义,不会再把这些值错误地递归拆成对象字段。
### 4. DI 注册与解析分离
旧的双参数 `ctx.inject(key, value)` 注册方式已移除。使用 `provide()` 注册,使用 `inject()` 解析:
```ts
// v2.9.x
ctx.inject('example', createExample())
const example = ctx.get('example')
// v2.10.0
ctx.provide('example', () => createExample())
const example = ctx.inject('example')
```
`provide()` 支持依赖列表与 `singleton` / `transient` 生命周期。自定义 TypeScript 服务键应通过声明合并扩展 `Services`。
### 5. `ctx.conf` 与 `ctx.$root` 不可替换
两者仍可读取,但不能整体赋值。运行时直接赋值会抛出带迁移提示的错误:
```ts
// 错误
ctx.conf = nextConf
ctx.$root = nextElement
// 正确
ctx.updateConf(nextConf)
const conf = ctx.getConf()
const root = ctx.getEl()
```
如需更换根元素,请销毁旧实例并使用新的 `el` 创建实例。
### 6. `Artalk.update()` 返回 `void`
`update()` 不再返回 Artalk 实例。将链式调用拆开:
```js
// v2.9.x
artalk.update({ pageKey: '/next' }).reload()
// v2.10.0
artalk.update({ pageKey: '/next' })
artalk.reload()
```
### 7. 插件初始化与 `created` 时点
内置插件、本地插件和网络插件现在都会在本地与远程配置完成合并后初始化。网络插件不会再看到仅来自某一端的临时配置。
`created` 会在所有插件完成初始化后触发,随后触发 `mounted`。依赖旧时序的插件应把初始化前置逻辑放在插件函数内,把需要完整插件环境的逻辑放到 `created` 监听器中。
### 8. Vote HTTP API 与客户端方法
旧的投票端点:
```http
POST /api/v2/votes/comment_up/123
```
新端点:
```http
GET /api/v2/votes/comment/123
POST /api/v2/votes/comment/123/up
```
`target_name` 只能为 `comment` 或 `page`,`choice` 只能为 `up` 或 `down`。
生成的客户端方法同步调整:
```ts
await api.votes.getVote('comment', 123)
await api.votes.createVote('comment', 123, 'up', { name, email })
```
旧端点和旧 `votes.vote()` 方法不保留兼容适配器。
### 9. SQLite 驱动改为纯 Go 实现
服务端 SQLite 驱动由基于 `mattn/go-sqlite3` 的 CGO 实现切换为 `github.com/libtnb/sqlite` 提供的纯 Go 实现。正式构建不再依赖本地 C 编译器、`CGO_ENABLED=1` 或 `GORELEASER_CROSS_VERSION` 对应的交叉编译工具链。
SQLite 磁盘文件格式没有改变,现有 `db.file` 数据库可以直接使用,普通部署无需修改数据库配置。升级前仍建议备份数据库;首次使用新版本启动时,Artalk 会照常执行数据库结构迁移。
自行编译或将 Artalk 作为 Go 模块嵌入的项目需要注意:
- 可以删除 SQLite 构建所需的 C 编译器、CGO 和 Goreleaser Cross 配置。
- 不要再依赖 `mattn/go-sqlite3` 专属的构建标签、DSN 参数、C API 或动态扩展加载行为。
- 如有自定义 SQLite 编译选项、扩展或原生备份工具集成,应使用 v2.10.0 重新执行迁移、并发读写和备份恢复测试。
此变更不影响使用 MySQL、PostgreSQL、MSSQL 的部署。
### 10. 默认配置和数据目录采用 XDG 规则
未指定 `--workdir` / `-w` 时,Artalk 不再始终把启动目录作为工作目录。v2.10.0 的默认查找顺序如下:
- 配置文件:启动目录、启动目录下的 `data`、`$XDG_CONFIG_HOME/artalk`、`/etc/artalk`。
- 数据目录:如果启动目录存在 `./data` 则保持旧工作目录;否则依次使用 `$XDG_DATA_HOME/artalk`、`/var/lib/artalk`,均不存在时创建 `$XDG_DATA_HOME/artalk`。
配置文件会在隐式切换数据目录前完成定位,但配置中的数据库、日志、上传目录等相对路径,以及 `import`、`export`、`gen` 等命令的相对文件参数,仍然基于最终工作目录解析。
现有目录中已经包含 `./data` 的常规部署保持原行为。官方 Docker 镜像也显式指定了工作目录和配置文件,不受此变化影响。
如果二进制部署需要完全保持旧路径语义,请显式指定工作目录和配置文件:
```bash
artalk -w /srv/artalk -c /srv/artalk/artalk.yml server
```
升级前应检查 `db.file`、`log.filename`、`img_upload.path`、IP 数据库和关键词文件等所有相对路径,避免在新的数据目录中意外创建空数据库或文件。
### 11. 源码构建要求 Go 1.26.5
`go.mod` 的最低 Go 版本从 1.22.7 提升到 1.26.5。该变化影响 `go install`、自行编译、发行版打包和将 Artalk 纳入 Go 构建流程的下游项目。
正式二进制和 Docker 镜像用户无需安装 Go。源码构建环境应升级到 Go 1.26.5;禁用 Go Toolchain 自动下载或处于离线环境的 CI 必须提前安装对应工具链。
### 12. 应用时区在进程初始化时固定
`timezone` 配置和 `ATK_TIMEZONE` 环境变量继续保留,用于设置独立于宿主系统或容器 `TZ` 的 Artalk 应用时区:
```yaml
timezone: Asia/Shanghai
```
```bash
ATK_TIMEZONE=Asia/Shanghai artalk server
```
时区属于进程级全局状态,v2.10.0 只在 Artalk 进程初始化时设置一次,运行期间不支持动态切换。修改配置文件、环境变量或控制中心中的时区后,必须完整重启 Artalk 进程或容器;仅调用控制中心的“应用配置”不足以切换时区。
### 13. `marked` 18 与现代包入口
默认 Artalk 构建继续内置 Markdown 能力;ArtalkLite 不把 `marked` 打包进自身,需要下游提供兼容的 `marked@^18.0.3`。
Artalk 使用的 `Marked`、`Renderer`、`parse()`、`setOptions()`、`use()` 和 Tokenizer Extension 均不是 v18 专属 API,但 v14 到 v18 的上游解析细节仍可能影响自定义 Renderer、Tokenizer 或快照输出。使用这些扩展的项目应重新运行 Markdown 测试。
Node.js 10 和依赖旧式包解析的工具链不再受支持。Artalk 仓库开发与构建使用 Node.js 22.19 及以上。
## 保留的兼容入口
为了给主题和第三方插件留出迁移时间,以下入口在 v2.10.0 仍然存在,但已经标记为 `deprecated`:
| 旧入口 | 推荐入口 |
| --- | --- |
| `ContextApi` | `Context` |
| `DataManagerApi` | `DataManager` |
| `EditorApi` | `Editor` |
| `ctx.data` | `ctx.getData()` 或 `ctx.inject('data')` |
| `ctx.user` | `ctx.getUser()` 或 `ctx.inject('user')` |
| `ctx.editor` | `ctx.inject('editor')` |
| `ctx.list` | `ctx.inject('list')` |
| `ctx.layerManager` | `ctx.inject('layers')` |
| `ctx.checkerLauncher` | `ctx.inject('checkers')` |
| `ctx.sidebarLayer` | `ctx.inject('sidebar')` |
| `ctx.editorPlugs` | `ctx.inject('editorPlugs')` |
| `editor.$el` | `editor.getEl()` |
| `editor.getPlugs()` | `editor.getPlugins()` |
| `editor.setReply()` | `editor.setReplyComment()` |
这些别名仅用于过渡;新代码应直接使用推荐入口。完整公开类型请查看 [TypeDoc](https://artalk.js.org/typedoc/)。
## 其他变更
- 评论回复按钮改用组件实现。
- Sidebar 与 Auth UI 已适配新的客户端类型。
- 新增 npm 发布版本检查与插件注册表生成流程。
- 固定 Swagger TypeScript 客户端生成器版本并更新命令,避免不同开发环境通过 `npx` 获取不兼容版本。
- 更新 GitHub Actions 运行时及制品、缓存、Docker、CodeQL 和 Codecov 相关 Actions;工作流输入与发布产物布局保持不变。
- 更新前端、Go 和文档依赖,并清理旧测试项目与 CI 配置。
完整提交差异可查看 [`v2.9.1...v2.10.0`](https://github.com/ArtalkJS/Artalk/compare/v2.9.1...v2.10.0)。
+8 -4
View File
@@ -138,15 +138,19 @@ typecho-20220424-202246.artrans
### Commento
你可在 Commento 后台导出 JSON 格式的数据文件,然后使用[转换工具](#转换工具)进行转换。
前往 Commento 后台,依次点击「General - Export」,导出 JSON 格式的数据文件,然后使用[转换工具](#转换工具)进行转换。
【图示,待补充...】
![](/images/transfer/commento.png)
### Twikoo
[Twikoo](https://twikoo.js.org/) 是一款基于腾讯云开发的评论系统,可前往 [腾讯云后台](https://console.cloud.tencent.com/tcb) 导出 JSON 格式的评论数据,然后使用[转换工具](#转换工具)进行转换。
[Twikoo](https://twikoo.js.org/) 可以通过所在管理面板中的「导出」板块获取JSON 文件,然后使用[转换工具](#转换工具)进行转换。
<img src="/images/transfer/tencent-tcb.png" style="max-width: 480px;">
![](/images/transfer/twikoo-control.png)
除此之外,对于腾讯云一键部署的Twikoo,可前往 [腾讯云后台](https://console.cloud.tencent.com/tcb) 导出 JSON 格式的评论数据。
![](/images/transfer/tencent-tcb.png)
### Artalk v1 (PHP 旧版后端)
+12 -11
View File
@@ -15,19 +15,20 @@
"update:readme": "npx tsx ./scripts/update-readme.ts"
},
"devDependencies": {
"@headlessui/react": "^2.1.10",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react-swc": "^3.7.1",
"i18next": "^23.16.0",
"i18next-browser-languagedetector": "^8.0.0",
"@headlessui/react": "^2.2.10",
"@types/node": "25.9.5",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react-swc": "^4.3.2",
"i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.1",
"include-media": "^2.0.0",
"normalize.css": "^8.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.3",
"react-icons": "5.3.0",
"usehooks-ts": "^3.1.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-i18next": "^17.0.10",
"react-icons": "5.7.0",
"usehooks-ts": "^3.1.1",
"vanilla-lazyload": "^19.1.3"
}
}
+4 -3
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
export const useIsElementVisible = (target: React.RefObject<HTMLDivElement>) => {
export const useIsElementVisible = (target: React.RefObject<HTMLDivElement | null>) => {
const [isVisible, setIsVisible] = useState(false)
useEffect(() => {
@@ -13,10 +13,11 @@ export const useIsElementVisible = (target: React.RefObject<HTMLDivElement>) =>
},
)
target.current && observer.observe(target.current)
const targetEl = target.current
targetEl && observer.observe(targetEl)
return () => {
target.current && observer.unobserve(target.current)
targetEl && observer.unobserve(targetEl)
}
}, [target])
+1 -1
View File
@@ -11,7 +11,7 @@ export const en = {
docs: 'Docs',
changelog: 'Changelog',
feature_slight_title: 'SLIGHT',
feature_slight_desc_line_1: 'A native JS client, framework-agnostic,',
feature_slight_desc_line_1: 'A native JS client, framework-agnostic, ',
feature_slight_desc_line_2:
'compact in size, fast to load, and instantly responsive to every interaction.',
feature_swift_title: 'SWIFT',
+10 -10
View File
@@ -4,21 +4,21 @@ export const fr: MessageSchema = {
home_title: 'Artalk | Système de commentaires auto-hébergé alimenté par Golang',
get_artalk: 'Obtenir Artalk',
get_artalk_link: 'https://artalk.js.org/guide/intro.html',
slogan_line_1: 'Auto-hébergé',
slogan_line_2: 'Commentaire',
slogan_line_3: 'Système',
slogan_line_1: 'Système de',
slogan_line_2: 'commentaires',
slogan_line_3: 'auto-hébergé',
nav_docs_link: 'https://artalk.js.org/guide/intro.html',
nav_changelog_link: 'https://github.com/ArtalkJS/Artalk/blob/master/CHANGELOG.md',
github_link: 'https://github.com/ArtalkJS/Artalk',
docs: 'Documentation',
changelog: 'Journal des modifications',
feature_slight_title: 'Léger',
feature_slight_desc_line_1: 'Programme frontal en JS natif, sans dépendance à un framework,',
feature_slight_desc_line_1: 'Programme frontal en JS natif, sans dépendance à un framework, ',
feature_slight_desc_line_2:
'taille compacte, chargement rapide et réponse instantanée à chaque interaction.',
feature_swift_title: 'Rapide',
feature_swift_desc_line_1:
'Artalk est écrit en Golang, offrant rapidité, multiplateforme et facilité de déploiement.',
'Artalk est écrit en Golang, offrant rapidité, compatibilité multiplateforme et facilité de déploiement.',
feature_swift_desc_line_2:
'Nous proposons également une image Docker pour un déploiement encore plus simple.',
feature_swift_self_compile: 'Vous souhaitez compiler le code vous-même ?',
@@ -43,7 +43,7 @@ export const fr: MessageSchema = {
'Artalk est un logiciel open-source, auto-hébergé et contrôlable en toute transparence.',
feature_safe_desc_line_2:
'Artalk inclut un outil de migration pour transférer facilement les commentaires.',
feature_safe_desc_line_3: 'Nous priorisons la sécurité et mettons à jour régulièrement.',
feature_safe_desc_line_3: 'Nous priorisons la sécurité et mettons le site à jour régulièrement.',
feature_safe_report_audit_hint: 'Vous avez trouvé un problème de sécurité ?',
feature_safe_report_audit_btn: 'Signaler maintenant',
feature_safe_report_audit_link: 'https://github.com/ArtalkJS/Artalk/issues',
@@ -82,7 +82,7 @@ export const fr: MessageSchema = {
feature_sidebar_name: 'Barre latérale',
feature_sidebar_desc: 'Gestion rapide, navigation intuitive',
feature_social_login_name: 'Connexion via réseaux sociaux',
feature_social_login_desc: 'Connexion rapide avec un compte réseau social',
feature_social_login_desc: 'Connexion rapide avec un compte de réseau social',
feature_email_notification_name: 'Notification par e-mail',
feature_email_notification_desc: "Plusieurs méthodes d'envoi et modèles d'e-mail",
feature_diverse_push_name: 'Notifications diverses',
@@ -153,7 +153,7 @@ export const fr: MessageSchema = {
intro_stack_card_3_text: 'Bonjour',
concept_section_locales_title: 'Localisation multilingue',
concept_section_locales_desc:
"Prise en charge de plusieurs langues, telles que l'anglais, le chinois, le français, le japonais, le coréen et le russe.",
"Nous prenons en charge plusieurs langues, telles que l'anglais, le chinois, le français, le japonais, le coréen et le russe.",
concept_section_docker_title: 'Installation avec Docker',
concept_section_docker_desc_line_1:
'Nous fournissons une image Docker pour une installation rapide en un seul clic.',
@@ -161,7 +161,7 @@ export const fr: MessageSchema = {
'Construit avec Golang, il est compatible avec les déploiements multi-plateformes, vous permettant de le lancer où que vous soyez.',
concept_section_open_source_title: 'Open Source',
concept_section_open_source_desc_line_1:
'Artalk est un logiciel libre et open source, axé sur la confidentialité et la sécurité des données.',
'Artalk est un logiciel gratuit et open source, axé sur la confidentialité et la sécurité des données.',
concept_section_open_source_desc_line_2:
"En maintenance continue depuis {{year}} ans, nous proposons des mises à jour régulières pour améliorer l'expérience utilisateur et garantir la stabilité.",
feature_community_title: 'Élargissez votre expérience',
@@ -171,5 +171,5 @@ export const fr: MessageSchema = {
any_website_or_blog: "n'importe quel site web ou blog",
see_more_on_github: 'Voir plus sur GitHub',
star_proposal_line_1: "Si vous aimez Artalk, n'hésitez pas à lui donner une étoile 🌟",
star_proposal_line_2: 'Cela nous serait très utile !',
star_proposal_line_2: 'Ça nous ferait vraiment plaisir !',
}
+4 -1
View File
@@ -4,6 +4,7 @@
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["node", "vite/client"],
"skipLibCheck": true,
/* Bundler mode */
@@ -11,7 +12,9 @@
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"composite": true,
"emitDeclarationOnly": true,
"declarationDir": ".tsbuildinfo-dts",
"jsx": "react-jsx",
/* Linting */
+1 -2
View File
@@ -3,8 +3,7 @@
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
"moduleResolution": "bundler"
},
"include": ["vite.config.ts"]
}
+1
View File
@@ -0,0 +1 @@
{"fileNames":["../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es5.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.dom.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.es2025.float16.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.decorators.d.ts","../../node_modules/.pnpm/typescript@6.0.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../../node_modules/.pnpm/@types+react@19.2.17/node_modules/@types/react/global.d.ts","../../node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts","../../node_modules/.pnpm/@types+react@19.2.17/node_modules/@types/react/index.d.ts","../../node_modules/.pnpm/@types+react@19.2.17/node_modules/@types/react/jsx-runtimeLine truncated
+2 -1
View File
@@ -8,7 +8,8 @@ export default defineConfig({
rollupOptions: {
output: {
assetFileNames: (assetInfo) => {
const extType = assetInfo.name.split('.')[1]
const assetName = assetInfo.names?.[0] ?? assetInfo.name ?? ''
const extType = assetName.split('.').pop() ?? ''
if (/png|jpe?g|svg|gif|tiff|bmp|ico/i.test(extType)) {
return `landing/img/[name]-[hash][extname]`
+173 -8
View File
@@ -2431,6 +2431,149 @@ const docTemplate = `{
}
}
},
"/sso/exchange": {
"post": {
"description": "Validates a third-party OIDC access token by calling the issuer's /userinfo endpoint, requires a verified email claim, then mints an Artalk session JWT. Use when the surrounding application already runs OIDC and you want Artalk comments to inherit that session without showing Artalk's own login UI.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Exchange an external IdP access token for an Artalk JWT",
"operationId": "AuthSSOExchange",
"parameters": [
{
"description": "External SSO token",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.ParamsAuthSSOExchange"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.ResponseUserLogin"
}
},
"400": {
"description": "Bad Request",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"401": {
"description": "Unauthorized",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"404": {
"description": "Not Found",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"502": {
"description": "Bad Gateway",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/stats/{type}": {
"get": {
"description": "Get the statistics of various data analysis",
@@ -3566,6 +3709,24 @@ const docTemplate = `{
"$ref": "#/definitions/handler.ResponseVote"
}
},
"400": {
"description": "Bad Request",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"403": {
"description": "Forbidden",
"schema": {
@@ -3801,10 +3962,6 @@ const docTemplate = `{
},
"common.JSONResult": {
"type": "object",
"required": [
"data",
"msg"
],
"properties": {
"data": {
"description": "Data"
@@ -3829,7 +3986,6 @@ const docTemplate = `{
"date",
"email_encrypted",
"id",
"ip_region",
"is_allow_reply",
"is_collapsed",
"is_pending",
@@ -4128,6 +4284,18 @@ const docTemplate = `{
"type": "object",
"additionalProperties": true
},
"handler.ParamsAuthSSOExchange": {
"type": "object",
"required": [
"token"
],
"properties": {
"token": {
"description": "External IdP access token (e.g. an Auth0 access token)",
"type": "string"
}
}
},
"handler.ParamsCaptchaVerify": {
"type": "object",
"required": [
@@ -4731,7 +4899,6 @@ const docTemplate = `{
"date",
"email_encrypted",
"id",
"ip_region",
"is_allow_reply",
"is_collapsed",
"is_pending",
@@ -4854,7 +5021,6 @@ const docTemplate = `{
"required": [
"comments",
"count",
"page",
"roots_count"
],
"properties": {
@@ -4885,7 +5051,6 @@ const docTemplate = `{
"date",
"email_encrypted",
"id",
"ip_region",
"is_allow_reply",
"is_collapsed",
"is_pending",
+5 -4
View File
@@ -10,11 +10,12 @@
},
"private": true,
"scripts": {
"swagger:build": "npx @redocly/cli build-docs --output ./dist/http-api.html",
"swagger:serve": "npx @redocly/cli preview-docs",
"swagger:build-http-client": "npx swagger-typescript-api -n v2 -p ./swagger.json -o ../../ui/artalk/src/api --responses --sort-types"
"swagger:build": "redocly build-docs --output ./dist/http-api.html",
"swagger:serve": "redocly preview",
"swagger:build-http-client": "swagger-typescript-api generate -n v2 -p ./swagger.json -o ../../ui/artalk/src/api --responses --sort-types && pnpm --dir ../.. exec prettier --write --ignore-path /dev/null --no-semi ui/artalk/src/api/v2.ts"
},
"devDependencies": {
"@redocly/cli": "1.25.7"
"@redocly/cli": "2.40.0",
"swagger-typescript-api": "13.12.6"
}
}
+11 -16
View File
@@ -1,5 +1,3 @@
organization: Artalk
extends:
- recommended
@@ -7,17 +5,14 @@ apis:
artalk@v2:
root: ./swagger.json
theme:
openapi:
schemaExpansionLevel: 2
defaultSampleLanguage: curl
hideLogo: true
generateCodeSamples:
languages:
- lang: curl
- lang: Python
- lang: Go
- lang: Node.js
- lang: JavaScript
- lang: PHP
- lang: Java
openapi:
schemasExpansionLevel: 2
codeSamples:
languages:
- lang: curl
- lang: Python
- lang: Go
- lang: Node.js
- lang: JavaScript
- lang: PHP
- lang: Java
+173 -8
View File
@@ -2424,6 +2424,149 @@
}
}
},
"/sso/exchange": {
"post": {
"description": "Validates a third-party OIDC access token by calling the issuer's /userinfo endpoint, requires a verified email claim, then mints an Artalk session JWT. Use when the surrounding application already runs OIDC and you want Artalk comments to inherit that session without showing Artalk's own login UI.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Exchange an external IdP access token for an Artalk JWT",
"operationId": "AuthSSOExchange",
"parameters": [
{
"description": "External SSO token",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.ParamsAuthSSOExchange"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.ResponseUserLogin"
}
},
"400": {
"description": "Bad Request",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"401": {
"description": "Unauthorized",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"404": {
"description": "Not Found",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"502": {
"description": "Bad Gateway",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
}
}
}
},
"/stats/{type}": {
"get": {
"description": "Get the statistics of various data analysis",
@@ -3559,6 +3702,24 @@
"$ref": "#/definitions/handler.ResponseVote"
}
},
"400": {
"description": "Bad Request",
"schema": {
"allOf": [
{
"$ref": "#/definitions/handler.Map"
},
{
"type": "object",
"properties": {
"msg": {
"type": "string"
}
}
}
]
}
},
"403": {
"description": "Forbidden",
"schema": {
@@ -3794,10 +3955,6 @@
},
"common.JSONResult": {
"type": "object",
"required": [
"data",
"msg"
],
"properties": {
"data": {
"description": "Data"
@@ -3822,7 +3979,6 @@
"date",
"email_encrypted",
"id",
"ip_region",
"is_allow_reply",
"is_collapsed",
"is_pending",
@@ -4121,6 +4277,18 @@
"type": "object",
"additionalProperties": true
},
"handler.ParamsAuthSSOExchange": {
"type": "object",
"required": [
"token"
],
"properties": {
"token": {
"description": "External IdP access token (e.g. an Auth0 access token)",
"type": "string"
}
}
},
"handler.ParamsCaptchaVerify": {
"type": "object",
"required": [
@@ -4724,7 +4892,6 @@
"date",
"email_encrypted",
"id",
"ip_region",
"is_allow_reply",
"is_collapsed",
"is_pending",
@@ -4847,7 +5014,6 @@
"required": [
"comments",
"count",
"page",
"roots_count"
],
"properties": {
@@ -4878,7 +5044,6 @@
"date",
"email_encrypted",
"id",
"ip_region",
"is_allow_reply",
"is_collapsed",
"is_pending",
+98 -7
View File
@@ -45,9 +45,6 @@ definitions:
msg:
description: Message
type: string
required:
- data
- msg
type: object
common.Map:
additionalProperties: true
@@ -110,7 +107,6 @@ definitions:
- date
- email_encrypted
- id
- ip_region
- is_allow_reply
- is_collapsed
- is_pending
@@ -276,6 +272,14 @@ definitions:
handler.Map:
additionalProperties: true
type: object
handler.ParamsAuthSSOExchange:
properties:
token:
description: External IdP access token (e.g. an Auth0 access token)
type: string
required:
- token
type: object
handler.ParamsCaptchaVerify:
properties:
value:
@@ -755,7 +759,6 @@ definitions:
- date
- email_encrypted
- id
- ip_region
- is_allow_reply
- is_collapsed
- is_pending
@@ -802,7 +805,6 @@ definitions:
required:
- comments
- count
- page
- roots_count
type: object
handler.ResponseCommentUpdate:
@@ -863,7 +865,6 @@ definitions:
- date
- email_encrypted
- id
- ip_region
- is_allow_reply
- is_collapsed
- is_pending
@@ -2678,6 +2679,87 @@ paths:
summary: Update Site
tags:
- Site
/sso/exchange:
post:
consumes:
- application/json
description: Validates a third-party OIDC access token by calling the issuer's
/userinfo endpoint, requires a verified email claim, then mints an Artalk
session JWT. Use when the surrounding application already runs OIDC and you
want Artalk comments to inherit that session without showing Artalk's own
login UI.
operationId: AuthSSOExchange
parameters:
- description: External SSO token
in: body
name: body
required: true
schema:
$ref: '#/definitions/handler.ParamsAuthSSOExchange'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.ResponseUserLogin'
"400":
description: Bad Request
schema:
allOf:
- $ref: '#/definitions/handler.Map'
- properties:
msg:
type: string
type: object
"401":
description: Unauthorized
schema:
allOf:
- $ref: '#/definitions/handler.Map'
- properties:
msg:
type: string
type: object
"404":
description: Not Found
schema:
allOf:
- $ref: '#/definitions/handler.Map'
- properties:
msg:
type: string
type: object
"500":
description: Internal Server Error
schema:
allOf:
- $ref: '#/definitions/handler.Map'
- properties:
msg:
type: string
type: object
"502":
description: Bad Gateway
schema:
allOf:
- $ref: '#/definitions/handler.Map'
- properties:
msg:
type: string
type: object
"503":
description: Service Unavailable
schema:
allOf:
- $ref: '#/definitions/handler.Map'
- properties:
msg:
type: string
type: object
summary: Exchange an external IdP access token for an Artalk JWT
tags:
- Auth
/stats/{type}:
get:
consumes:
@@ -3311,6 +3393,15 @@ paths:
description: OK
schema:
$ref: '#/definitions/handler.ResponseVote'
"400":
description: Bad Request
schema:
allOf:
- $ref: '#/definitions/handler.Map'
- properties:
msg:
type: string
type: object
"403":
description: Forbidden
schema:
+13 -13
View File
@@ -4,7 +4,7 @@ import url from 'node:url'
import { fixupPluginRules } from '@eslint/compat'
import pluginCompat from 'eslint-plugin-compat'
import eslintJs from '@eslint/js'
import pluginTS from '@typescript-eslint/eslint-plugin'
import { defineConfig } from 'eslint/config'
import eslintConfigPrettier from 'eslint-config-prettier'
import pluginImportX from 'eslint-plugin-import-x'
import pluginReact from 'eslint-plugin-react'
@@ -17,13 +17,11 @@ import vueParser from 'vue-eslint-parser'
import pluginArtalk from 'eslint-plugin-artalk'
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
const tsProjects = ['./tsconfig.base.json', './ui/*/tsconfig.json', './docs/*/tsconfig.json']
export default eslintTs.config(
export default defineConfig(
eslintJs.configs.recommended,
...eslintTs.configs.recommended,
// @ts-expect-error the type of `pluginVue` is not compatible with the latest `eslint` v9 package yet
...pluginVue.configs['flat/recommended'],
eslintTs.configs.recommended,
pluginVue.configs['flat/recommended'],
pluginCompat.configs['flat/recommended'],
/* Global Ignores */
@@ -38,9 +36,12 @@ export default eslintTs.config(
'local',
'test',
'**/.vitepress/cache',
'**/.vitepress/.temp',
'**/*.config.js',
'**/*.config.ts',
'**/*.d.ts',
'**/*.d.cts',
'**/*.d.mts',
],
},
@@ -51,7 +52,7 @@ export default eslintTs.config(
parser: vueParser,
parserOptions: {
parser: eslintTs.parser,
project: tsProjects,
projectService: true,
tsconfigRootDir: __dirname,
globals: {
...globals.browser,
@@ -64,7 +65,6 @@ export default eslintTs.config(
},
},
plugins: {
'@typescript-eslint': pluginTS,
'import-x': pluginImportX,
artalk: pluginArtalk,
},
@@ -90,7 +90,7 @@ export default eslintTs.config(
},
'import-x/resolver': {
typescript: {
project: tsProjects,
project: './tsconfig.json',
},
node: true,
},
@@ -146,14 +146,14 @@ export default eslintTs.config(
...pluginReact.configs['jsx-runtime'].rules,
...pluginReactHooks.configs.recommended.rules,
'react-refresh/only-export-components': 'warn',
'react-hooks/immutability': 'off',
'react-hooks/set-state-in-effect': 'off',
'react/prop-types': 'off',
},
languageOptions: {
...pluginReact.configs.recommended.languageOptions,
},
settings: {
react: {
version: '18.3',
version: '19.2',
},
},
},
+93 -94
View File
@@ -1,94 +1,95 @@
module github.com/artalkjs/artalk/v2
go 1.22.7
go 1.26.5
require (
github.com/adrg/xdg v0.5.0
github.com/adrg/xdg v0.5.3
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-20230905024940-24af94b03874
github.com/cheggaaa/pb/v3 v3.1.5
github.com/eko/gocache/lib/v4 v4.1.6
github.com/eko/gocache/store/bigcache/v4 v4.2.2
github.com/eko/gocache/store/memcache/v4 v4.2.2
github.com/eko/gocache/store/redis/v4 v4.2.2
github.com/fatih/color v1.17.0
github.com/go-testfixtures/testfixtures/v3 v3.12.0
github.com/goccy/go-yaml v1.12.0
github.com/gofiber/fiber/v2 v2.52.5
github.com/gofiber/swagger v1.1.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c
github.com/cheggaaa/pb/v3 v3.2.0
github.com/eko/gocache/lib/v4 v4.2.3
github.com/eko/gocache/store/bigcache/v4 v4.2.4
github.com/eko/gocache/store/memcache/v4 v4.2.4
github.com/eko/gocache/store/redis/v4 v4.2.6
github.com/fatih/color v1.19.0
github.com/go-sql-driver/mysql v1.10.0
github.com/go-testfixtures/testfixtures/v3 v3.19.0
github.com/goccy/go-yaml v1.19.2
github.com/gofiber/fiber/v2 v2.52.14
github.com/gofiber/swagger v1.1.1
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/iancoleman/strcase v0.3.0
github.com/jedib0t/go-pretty/v6 v6.6.0
github.com/jedib0t/go-pretty/v6 v6.8.3
github.com/jeremywohl/flatten v1.0.1
github.com/knadh/koanf v1.5.0
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6
github.com/markbates/goth v1.80.0
github.com/mattn/go-colorable v0.1.13
github.com/libtnb/sqlite v1.2.1
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260709160242-cd40e3a1d532
github.com/markbates/goth v1.82.0
github.com/mattn/go-colorable v0.1.15
github.com/microcosm-cc/bluemonday v1.0.27
github.com/nikoksr/notify v1.0.0
github.com/nikoksr/notify v1.5.0
github.com/qwqcode/go-aliyun-email v0.0.0-20180120030821-cb6e7b1382bf
github.com/redis/go-redis/v9 v9.6.1
github.com/redis/go-redis/v9 v9.21.0
github.com/rhysd/go-github-selfupdate v1.2.3
github.com/samber/lo v1.47.0
github.com/spf13/cobra v1.8.1
github.com/samber/lo v1.53.0
github.com/spf13/cobra v1.10.2
github.com/steambap/captcha v1.4.1
github.com/stretchr/testify v1.9.0
github.com/swaggo/swag v1.16.3
github.com/tidwall/gjson v1.18.0
github.com/stretchr/testify v1.11.1
github.com/swaggo/swag v1.16.6
github.com/tidwall/gjson v1.19.0
github.com/vmihailenco/msgpack v4.0.4+incompatible
github.com/yuin/goldmark v1.7.4
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.27.0
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0
golang.org/x/sync v0.8.0
golang.org/x/term v0.24.0
golang.org/x/text v0.18.0
github.com/yuin/goldmark v1.8.4
go.uber.org/zap v1.28.0
golang.org/x/crypto v0.54.0
golang.org/x/exp v0.0.0-20260718201538-764159d718ef
golang.org/x/sync v0.22.0
golang.org/x/term v0.45.0
golang.org/x/text v0.40.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.5.7
gorm.io/driver/postgres v1.5.9
gorm.io/driver/sqlite v1.5.6
gorm.io/driver/sqlserver v1.5.3
gorm.io/gorm v1.25.12
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlserver v1.6.3
gorm.io/gorm v1.31.2
)
require (
cloud.google.com/go/compute/metadata v0.5.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/ClickHouse/ch-go v0.62.0 // indirect
github.com/ClickHouse/clickhouse-go/v2 v2.29.0 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/spec v0.22.9 // indirect
github.com/go-openapi/swag/conv v0.27.3 // indirect
github.com/go-openapi/swag/jsonutils v0.27.3 // indirect
github.com/go-openapi/swag/loading v0.27.3 // indirect
github.com/go-openapi/swag/pools v0.27.3 // indirect
github.com/go-openapi/swag/stringutils v0.27.3 // indirect
github.com/go-openapi/swag/typeutils v0.27.3 // indirect
github.com/go-openapi/swag/yamlutils v0.27.3 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
@@ -96,67 +97,65 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.10 // indirect
github.com/klauspost/compress v1.19.1 // indirect
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/jwx v1.2.30 // indirect
github.com/lestrrat-go/jwx v1.2.31 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/line/line-bot-sdk-go v7.8.0+incompatible // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/markbates/going v1.0.3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.23 // indirect
github.com/microsoft/go-mssqldb v1.7.2 // indirect
github.com/mattn/go-isatty v0.0.23 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/microsoft/go-mssqldb v1.10.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/mrjones/oauth v0.0.0-20190623134757-126b35219450 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/gomega v1.34.2 // indirect
github.com/paulmach/orb v0.11.1 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/onsi/gomega v1.40.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.20.4 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.24.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/slack-go/slack v0.14.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/swaggo/files/v2 v2.0.1 // indirect
github.com/slack-go/slack v0.27.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/swaggo/files/v2 v2.0.2 // 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/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/ulikunitz/xz v0.5.16 // indirect
github.com/utahta/go-linenotify v0.5.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.56.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/valyala/fasthttp v1.72.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.30.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/image v0.20.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/oauth2 v0.23.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/tools v0.25.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/image v0.44.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.34.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
modernc.org/libc v1.74.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.54.0 // indirect
)
+317 -265
View File
@@ -1,44 +1,40 @@
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=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.4.0/go.mod h1:ON4tFdPTwRcgWEaVDrN3584Ef+b7GgSJaXxe5fW9t4M=
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/azcore v1.6.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.2.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/ClickHouse/ch-go v0.62.0 h1:eXH0hytXeCEEZHgMvOX9IiW7wqBb4w1MJMp9rArbkrc=
github.com/ClickHouse/ch-go v0.62.0/go.mod h1:uzso52/PD9+gZj7tL6XAo8/EYDrx7CIwNF4c6PnO6S0=
github.com/ClickHouse/clickhouse-go/v2 v2.29.0 h1:Dj1w59RssRyLgGHXtYaWU0eIM1pJsu9nGPi/btmvAqw=
github.com/ClickHouse/clickhouse-go/v2 v2.29.0/go.mod h1:bLookq6qZJ4Ush/6tOAnJGh1Sf3Sa/nQoMn71p7ZCUE=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -46,8 +42,8 @@ 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.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
@@ -78,8 +74,8 @@ 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 v1.1.3 h1:MbidFZYom7DTFHD/YIs+eaI7kRy52kmWE/sy0xjo6E4=
github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c h1:6Gpm9YYUEQx2T9zMsYolQhr6sjwwGtFitSA0pQsa7a8=
github.com/bradfitz/gomemcache v0.0.0-20260422231931-4d751bb6e37c/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -88,34 +84,36 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk=
github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI=
github.com/cheggaaa/pb/v3 v3.2.0 h1:ziC7JV5/Ge2iZNa9ckxdXBxHHyPgC+p/QGzhWRoPlHU=
github.com/cheggaaa/pb/v3 v3.2.0/go.mod h1:KtXGzgipYGqY3avGtFmlQTgiT88AEFvc1LvPk7oA9fM=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw=
github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
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.6 h1:5WWIGISKhE7mfkyF+SJyWwqa4Dp2mkdX8QsZpnENqJI=
github.com/eko/gocache/lib/v4 v4.1.6/go.mod h1:HFxC8IiG2WeRotg09xEnPD72sCheJiTSr4Li5Ameg7g=
github.com/eko/gocache/store/bigcache/v4 v4.2.2 h1:zS8wjE/MqNQZOZMa19urItNIiVPE1CktJpmaGhPv9yE=
github.com/eko/gocache/store/bigcache/v4 v4.2.2/go.mod h1:B3EPikcLx486f5Xw3YtFjm3oWnKGYngxJW4v1/n5L5g=
github.com/eko/gocache/store/memcache/v4 v4.2.2 h1:VKfxytQ5bkcfF3LhmgkrqRiEU2yCN2/rJBUvF1fKZJw=
github.com/eko/gocache/store/memcache/v4 v4.2.2/go.mod h1:9lFU3tZPiej8E3J4ueZ0K9kIdiDQpRxu6WhtId5OsZA=
github.com/eko/gocache/store/redis/v4 v4.2.2 h1:Thw31fzGuH3WzJywsdbMivOmP550D6JS7GDHhvCJPA0=
github.com/eko/gocache/store/redis/v4 v4.2.2/go.mod h1:LaTxLKx9TG/YUEybQvPMij++D7PBTIJ4+pzvk0ykz0w=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eko/gocache/lib/v4 v4.2.3 h1:s78TFqEGAH3SbzP4N40D755JYT/aaGFKEPrsUtC1chU=
github.com/eko/gocache/lib/v4 v4.2.3/go.mod h1:Zus8mwmaPu1VYOzfomb+Dvx2wV7fT5jDRbHYtQM6MEY=
github.com/eko/gocache/store/bigcache/v4 v4.2.4 h1:Ak2qZYh7ioRoSts6xUObdnbNhKIZQxrVGO1kb21U8Ak=
github.com/eko/gocache/store/bigcache/v4 v4.2.4/go.mod h1:ty85W3DT2S5ZO3VG30UkwMQ1FmH8p6O0Y6FrQz7alUo=
github.com/eko/gocache/store/memcache/v4 v4.2.4 h1:k8bLwU7F3caEO43eC2dHZR0X+KsoF/aQfpsRgFap6Io=
github.com/eko/gocache/store/memcache/v4 v4.2.4/go.mod h1:DxmvRb6CIb8GKnNFgn+A+hhAEeFZRxThLo9+v3Z6TMs=
github.com/eko/gocache/store/redis/v4 v4.2.6 h1:btD3TIFAgujZAsvTEBTaDfNTSAf+GO32il0HQ5MIqCI=
github.com/eko/gocache/store/redis/v4 v4.2.6/go.mod h1:0PMef3sy4AonKqrxdnUsIKDAMtqNyJI4e6asTo00XrE=
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=
@@ -123,18 +121,14 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
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.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@@ -142,50 +136,59 @@ github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w=
github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU=
github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ=
github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc=
github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0=
github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE=
github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk=
github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI=
github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4=
github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs=
github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
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.12.0 h1:Ew0+c2o1mXSUqMwjuNup3MK/vw1HkLS3ILljX5C6lVE=
github.com/go-testfixtures/testfixtures/v3 v3.12.0/go.mod h1:13F0m6/DtqqSDso9IAVuhbZ4I7AiRAHrolmDMu9v5vY=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-testfixtures/testfixtures/v3 v3.19.0 h1:/Y0bars250zggm+1A2PvwaJQsJel7/tS4D/Hhwt66Bc=
github.com/go-testfixtures/testfixtures/v3 v3.19.0/go.mod h1:4/hVAuX2As0/ej3fLuAd+IvoCXV7/h2cj5nInI11uxM=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/swagger v1.1.0 h1:ff3rg1fB+Rp5JN/N8jfxTiZtMKe/9tB9QDc79fPiJKQ=
github.com/gofiber/swagger v1.1.0/go.mod h1:pRZL0Np35sd+lTODTE5The0G+TMHfNY+oC4hM2/i5m8=
github.com/gofiber/fiber/v2 v2.52.14 h1:Of3L+9qVFaQNwPlcmEdl5IIodHz8BSE0j37R7rWu4pE=
github.com/gofiber/fiber/v2 v2.52.14/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/swagger v1.1.1 h1:FZVhVQQ9s1ZKLHL/O0loLh49bYB5l1HEAgxDlcTtkRA=
github.com/gofiber/swagger v1.1.1/go.mod h1:vtvY/sQAMc/lGTUCg0lqmBL7Ht9O7uzChpbvJeJQINw=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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=
@@ -194,8 +197,6 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
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=
@@ -219,19 +220,21 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v0.0.0-20171113160352-8c31c18f31ed/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -247,7 +250,6 @@ github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1 h1:LqbZZ9sNMWVjeXS4NN5
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -277,7 +279,10 @@ github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
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 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
@@ -297,24 +302,12 @@ github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf h1:WfD7V
github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf/go.mod h1:hyb9oH7vZsitZCiBt0ZvifOrB+qc8PS5IiilCIb87rg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
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/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da h1:FjHUJJ7oBW4G/9j1KzlHaXL09LyMVM9rupS39lncbXk=
@@ -325,8 +318,8 @@ github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nD
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jedib0t/go-pretty/v6 v6.6.0 h1:wmZVuAcEkZRT+Aq1xXpE8IGat4vE5WXOMmBpbQqERXw=
github.com/jedib0t/go-pretty/v6 v6.6.0/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E=
github.com/jedib0t/go-pretty/v6 v6.8.3 h1:yVSk5aemoYHCvcrtqyXklwqcgHQIQzmy/oUzFlmffSQ=
github.com/jedib0t/go-pretty/v6 v6.8.3/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jeremywohl/flatten v1.0.1 h1:LrsxmB3hfwJuE+ptGOijix1PIfOoKLJ3Uee/mzbgtrs=
github.com/jeremywohl/flatten v1.0.1/go.mod h1:4AmD/VxjWcI5SRB0n6szE2A6s2fsNHDLO0nAlMHgfLQ=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -335,13 +328,10 @@ 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 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
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=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -350,9 +340,10 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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=
@@ -360,6 +351,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -368,57 +360,52 @@ 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 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
github.com/lestrrat-go/jwx v1.2.30 h1:VKIFrmjYn0z2J51iLPadqoHIVLzvWNa1kCsTqNDHYPA=
github.com/lestrrat-go/jwx v1.2.30/go.mod h1:vMxrwFhunGZ3qddmfmEm2+uced8MSI6QFWGTKygjSzQ=
github.com/lestrrat-go/jwx v1.2.31 h1:/OM9oNl/fzyldpv5HKZ9m7bTywa7COUfg8gujd9nJ54=
github.com/lestrrat-go/jwx v1.2.31/go.mod h1:eQJKoRwWcLg4PfD5CFA5gIZGxhPgoPYq9pZISdxLf0c=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libtnb/sqlite v1.2.1 h1:lrhN3yG49pj1gmtMMP6Es0lhKIkzDeqBul2EWOLyjpY=
github.com/libtnb/sqlite v1.2.1/go.mod h1:pGq13BirCjkM6ihkWWxs0PGFhBiwUyA0PqdSmgDWfmM=
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-20240510055607-89e20ab7b6c6 h1:YeIGErDiB/fhmNsJy0cfjoT8XnRNT9hb19xZ4MvWQDU=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20240510055607-89e20ab7b6c6/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260709160242-cd40e3a1d532 h1:jm6X6eUeR7CI5gFT6zMUtANhyvBiN0U1fWoANQ7e690=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20260709160242-cd40e3a1d532/go.mod h1:sj5LMpsqB4IWdwIrcmmBJM6m+rW/uOQLSGUPhKkqdh8=
github.com/markbates/going v1.0.3 h1:mY45T5TvW+Xz5A6jY7lf4+NLg9D8+iuStIHyR7M8qsE=
github.com/markbates/going v1.0.3/go.mod h1:fQiT6v6yQar9UD6bd/D4Z5Afbk9J6BBVBtLiyY4gp2o=
github.com/markbates/goth v1.80.0 h1:NnvatczZDzOs1hn9Ug+dVYf2Viwwkp/ZDX5K+GLjan8=
github.com/markbates/goth v1.80.0/go.mod h1:4/GYHo+W6NWisrMPZnq0Yr2Q70UntNLn7KXEFhrIdAY=
github.com/markbates/goth v1.82.0 h1:8j/c34AjBSTNzO7zTsOyP5IYCQCMBTRBHAbBt/PI0bQ=
github.com/markbates/goth v1.82.0/go.mod h1:/DRlcq0pyqkKToyZjsL2KgiA1zbF1HIjE7u2uC79rUk=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
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-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU=
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.10.0 h1:pHEt+Qz6YFPWqREq10mqSE524QQo+/QremwTCQht7TY=
github.com/microsoft/go-mssqldb v1.10.0/go.mod h1:mnG7lGa9iYJbzJqGCXyuQCegStKMr3kogDLD6+bmggg=
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=
@@ -442,7 +429,6 @@ 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.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450 h1:j2kD3MT1z4PXCiUllUJF9mWUESr9TWKS7iEKsQ/IipM=
github.com/mrjones/oauth v0.0.0-20190623134757-126b35219450/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
@@ -450,105 +436,106 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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/nikoksr/notify v1.0.0 h1:qe9/6FRsWdxBgQgWcpvQ0sv8LRGJZDpRB4TkL2uNdO8=
github.com/nikoksr/notify v1.0.0/go.mod h1:hPaaDt30d6LAA7/5nb0e48Bp/MctDfycCSs8VEgN29I=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nikoksr/notify v1.5.0 h1:mzkCw8eb0P+qHwgmGQyPPGqz4GH+07FJDr44Bs16T9k=
github.com/nikoksr/notify v1.5.0/go.mod h1:CEV9Bw9Y59K5oj7d8h83Xl32ATeL43ZEg9qTQsfwcCc=
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
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.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
github.com/onsi/gomega v1.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc=
github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
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.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU=
github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
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.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
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.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
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.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA=
github.com/prometheus/common v0.60.0/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw=
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
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.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
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.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
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=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
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.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
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/slack-go/slack v0.14.0 h1:6c0UTfbRnvRssZUsZ2qe0Iu07VAMPjRqOa6oX8ewF4k=
github.com/slack-go/slack v0.14.0/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/slack-go/slack v0.27.0 h1:VWOpUzOK6UAPCCQlFxl79jhv8a/b+GOSJMnWziDJ8B8=
github.com/slack-go/slack v0.27.0/go.mod h1:UEe+jmo9WLlwHB04qsOrTDvqM7Aa4rQL3O5wF3n0hx4=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
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=
@@ -558,70 +545,69 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/swaggo/files/v2 v2.0.1 h1:XCVJO/i/VosCDsJu1YLpdejGsGnBE9deRMpjN4pJLHk=
github.com/swaggo/files/v2 v2.0.1/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
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.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.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/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.16 h1:ld6NyySjx5lowVKwJvMRLnW5nxKX/xnpSiFYZ/Lxur0=
github.com/ulikunitz/xz v0.5.16/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw=
github.com/utahta/go-linenotify v0.5.0 h1:E1tJaB/XhqRY/iz203FD0MaHm10DjQPOq5/Mem2A3Gs=
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.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW8U=
github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI=
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/valyala/fasthttp v1.72.0 h1:R7kYdoWhn1ye1fVpP+cDHDJwYm3NkwLliwgzJ/Abg7M=
github.com/valyala/fasthttp v1.72.0/go.mod h1:zsbLTYqcpIktdQytlVBwIjY9La5d6bs990nBxWg8efk=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/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/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
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.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A=
go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
@@ -629,19 +615,24 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A=
golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
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=
@@ -652,8 +643,12 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
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=
@@ -670,26 +665,32 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
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.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
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=
@@ -702,8 +703,12 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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=
@@ -733,29 +738,45 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
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=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
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=
@@ -767,9 +788,15 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
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=
@@ -781,18 +808,17 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
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-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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -823,9 +849,8 @@ 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.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
@@ -854,18 +879,45 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/driver/sqlserver v1.5.3 h1:rjupPS4PVw+rjJkfvr8jn2lJ8BMhT4UW5FwuJY0P3Z0=
gorm.io/driver/sqlserver v1.5.3/go.mod h1:B+CZ0/7oFJ6tAlefsKoyxdgDCXJKSgwS2bMOQZT0I00=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/driver/sqlserver v1.6.3 h1:UR+nWCuphPnq7UxnL57PSrlYjuvs+sf1N59GgFX7uAI=
gorm.io/driver/sqlserver v1.6.3/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
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=
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.74.3 h1:a4J+Z8aVaxPyjyxRAdJzw246PqpcFGvVPnfT/AuM5Ws=
modernc.org/libc v1.74.3/go.mod h1:4H7h/MJ8wnjL8RAbp9v3OXgnk22X7MouHIhDbvP3gj4=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
+80
View File
@@ -0,0 +1,80 @@
"Account": "Hesap"
"Admin": "Yönetici"
"Admin access required": "Yönetici erişimi gerekli"
"Cannot reply to this comment": "Bu yoruma cevap verilemez"
"Captcha required": "Doğrulama (Captcha) gerekli"
"Checking for updates": "Güncellemeler kontrol ediliyor"
"Comment": "Yorum"
"Comment count": "Yorum sayısı"
"Comment failed": "Yorum başarısız"
"Config file read failed": "Yapılandırma dosyası okunamadı"
"Confirm to continue?": "Devam etmek istiyor musunuz?"
"Contains invalid URL": "Geçersiz URL içeriyor"
"Create admin account": "Yönetici hesabı oluştur"
"Current version is the latest": "Mevcut sürüm güncel"
"Downloading": "İndiriliyor"
"Email": "E-posta"
"Enabled": "Etkin"
"Enter {{name}}": "{{name}} girin"
"Export complete": "Dışa aktarma tamamlandı"
"Export error": "Dışa aktarma hatası"
"File": "Dosya"
"First comment": "İlk yorum"
"Image exceeds {{file_size}} limit": "Görsel {{file_size}} sınırını aşıyor"
"Image upload forbidden": "Görsel yükleme yasak"
"Import completed": "İçe aktarma tamamlandı"
"Import failed": "İçe aktarma başarısız"
"Importing": "İçe aktarılıyor"
"Invalid {{name}}": "Geçersiz {{name}}"
"Link": "Bağlantı"
"Login failed": "Giriş başarısız"
"Login required": "Giriş gerekli"
"Name": "Ad"
"New version available": "Yeni sürüm mevcut"
"No comment": "Yorum yok"
"Notify": "Bildir"
"Page": "Sayfa"
"Page fetch failed": "Sayfa getirme başarısız"
"Parent comment": "Üst yorum"
"Password": "Şifre"
"Password is incorrect": "Şifre yanlış"
"Password update failed": "Şifre güncelleme başarısız"
"Password updated": "Şifre güncellendi"
"Pending": "Beklemede"
"Please review": "Lütfen inceleyin"
"Reply": "Yanıtla"
"Restart failed: {{err}}": "Yeniden başlatma başarısız: {{err}}"
"Retype {{name}}": "{{name}} tekrar girin"
"Save failed": "Kaydetme başarısız"
"Services restart complete": "Hizmetlerin yeniden başlatılması tamamlandı"
"Site": "Site"
"Site `{{name}}` not found. Please create it in control center.": "`{{name}}` sitesi bulunamadı. Lütfen kontrol merkezinde oluşturun."
"Site name": "Site adı"
"Sub-comment": "Alt yorum"
"Target Site": "Hedef Site"
"Task executing in background, please wait...": "İşlem arka planda yürütülüyor, lütfen bekleyin..."
"Task in progress, please wait a moment": "İşlem devam ediyor, lütfen biraz bekleyin"
"Type": "Tür"
"URL Resolver": "URL Çözücü"
"Unspecified": "Belirtilmemiş"
"Unsupported formats": "Desteklenmeyen formatlar"
"Update complete": "Güncelleme tamamlandı"
"Update failed": "Güncelleme başarısız"
"Upload image via {{method}} failed": "{{method}} üzerinden görsel yükleme başarısız"
"User": "Kullanıcı"
"User not found": "Kullanıcı bulunamadı"
"Username": "Kullanıcı adı"
"Verification failed": "Doğrulama başarısız"
"Wrong captcha": "Yanlış doğrulama (Captcha)"
"Your Code - {{code}}": "Kodunuz - {{code}}"
"Your authentication token has expired. Please try signing in again.": "Kimlik doğrulama anahtarınızın süresi doldu. Lütfen tekrar giriş yapmayı deneyin."
"Your code is: {{code}}. Use it to verify your email and sign in Artalk. If you didn't request this, simply ignore this message.": "Kodunuz: {{code}}. E-postanızı doğrulamak ve Artalk'a giriş yapmak için kullanın. Bunu siz istemediyseniz bu mesajı görmezden gelebilirsiniz."
"{{count}} items imported": "{{count}} öğe içe aktarıldı"
"{{done}} of {{total}} done": "{{done}} / {{total}} tamamlandı"
"{{name}} already exists": "{{name}} zaten mevcut"
"{{name}} cannot be empty": "{{name}} boş olamaz"
"{{name}} creation failed": "{{name}} oluşturma başarısız"
"{{name}} deletion failed": "{{name}} silme başarısız"
"{{name}} is required": "{{name}} gerekli"
"{{name}} not found": "{{name}} bulunamadı"
"{{name}} save failed": "{{name}} kaydetme başarısız"
+1 -1
View File
@@ -75,7 +75,7 @@ func (c *AkismetChecker) Check(p *CheckerParams) (bool, error) {
return true, nil
}
return false, fmt.Errorf(respStr)
return false, fmt.Errorf("%s", respStr)
}
type AkismetReqParams struct {
+4
View File
@@ -1,6 +1,7 @@
package anti_spam
import (
"errors"
"fmt"
"os"
"strconv"
@@ -87,6 +88,9 @@ func (c *KeywordsChecker) Check(p *CheckerParams) (bool, error) {
func (c *KeywordsChecker) loadKeywords() error {
c.mux.Lock()
defer c.mux.Unlock()
if c.conf.FileSep == "" {
return errors.New("keywords file separator cannot be empty")
}
// 已加载过无需再次加载
if c.keywords != nil {
+15 -1
View File
@@ -108,9 +108,23 @@ func TestNewKeywordsChecker(t *testing.T) {
assert.False(t, ok)
})
t.Run("ErrorEmptySeparator", func(t *testing.T) {
checker := NewKeywordsChecker(&KeywordsCheckerConf{
Files: []string{kwFile1},
FileSep: "",
Mode: KwCheckerModeBlock,
})
ok, err := checker.Check(&CheckerParams{
Content: "关键词A",
})
assert.ErrorContains(t, err, "separator cannot be empty")
assert.False(t, ok)
})
t.Run("ErrorUnknownMode", func(t *testing.T) {
checker := NewKeywordsChecker(&KeywordsCheckerConf{
Mode: 999,
FileSep: "\n",
Mode: 999,
})
ok, err := checker.Check(&CheckerParams{})
assert.ErrorContains(t, err, "unknown mode")
+1 -1
View File
@@ -23,7 +23,7 @@ func RunImportArtrans(dao *dao.Dao, params *ImportParams, outputFunc ...func(str
// Read JSON
if params.JsonData == "" {
if params.JsonFile == "" {
err := fmt.Errorf(i18n.T("{{name}} is required", map[string]any{"name": "json_file:<JSON file path>"}))
err := fmt.Errorf("%s", i18n.T("{{name}} is required", map[string]any{"name": "json_file:<JSON file path>"}))
console.Error(err)
return err
}
+2 -2
View File
@@ -44,11 +44,11 @@ func importArtrans(tx *gorm.DB, params *ImportParams, comments []*entity.Artran)
isTrue := func(val string) bool { return val == "true" || val == "1" }
if len(comments) == 0 {
return fmt.Errorf(i18n.T("No comment"))
return fmt.Errorf("%s", i18n.T("No comment"))
}
if params.TargetSiteURL != "" && !utils.ValidateURL(params.TargetSiteURL) {
return fmt.Errorf(i18n.T("Invalid {{name}}", map[string]interface{}{"name": i18n.T("Target Site") + " " + "URL"}))
return fmt.Errorf("%s", i18n.T("Invalid {{name}}", map[string]interface{}{"name": i18n.T("Target Site") + " " + "URL"}))
}
console.Println()
+2 -2
View File
@@ -13,12 +13,12 @@ import (
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")}))
return "", fmt.Errorf("%s", 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 "", fmt.Errorf("file open failed: %w", err)
}
return string(buf), nil
+1 -1
View File
@@ -30,7 +30,7 @@ func QueryDBWithCache[T any](c *Cache, name string, queryDB func() (T, error)) (
// Store cache
if err := c.StoreCache(val, name); err != nil {
return nil, err
log.Warn("[Cache] failed to store database query result: ", err)
}
return val, nil
+102
View File
@@ -0,0 +1,102 @@
package cache
import (
"context"
"errors"
"sync/atomic"
"testing"
"time"
lib_cache "github.com/eko/gocache/lib/v4/cache"
"github.com/eko/gocache/lib/v4/marshaler"
"github.com/eko/gocache/lib/v4/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestQueryDBWithCacheReturnsDatabaseValueWhenStoreFails(t *testing.T) {
storeErr := errors.New("cache store unavailable")
cacheStore := &failureStore{
getErr: errors.New("cache miss"),
setErr: storeErr,
}
cacheInstance := newCacheWithStore(cacheStore)
defer cacheInstance.Close()
type user struct {
Name string
Email string
}
want := user{Name: "admin", Email: "admin@example.com"}
got, err := QueryDBWithCache(cacheInstance, t.Name(), func() (user, error) {
return want, nil
})
require.NoError(t, err)
assert.Equal(t, want, got)
assert.Equal(t, int32(1), cacheStore.setCalls.Load())
}
func TestQueryDBWithCacheReturnsDatabaseError(t *testing.T) {
dbErr := errors.New("database unavailable")
cacheStore := &failureStore{getErr: errors.New("cache miss")}
cacheInstance := newCacheWithStore(cacheStore)
defer cacheInstance.Close()
got, err := QueryDBWithCache(cacheInstance, t.Name(), func() (string, error) {
return "", dbErr
})
require.ErrorIs(t, err, dbErr)
assert.Empty(t, got)
assert.Equal(t, int32(0), cacheStore.setCalls.Load())
}
func newCacheWithStore(cacheStore store.StoreInterface) *Cache {
ctx, cancel := context.WithCancel(context.Background())
instance := lib_cache.New[any](cacheStore)
return &Cache{
ttl: time.Minute,
ctx: ctx,
cancel: cancel,
instance: instance,
marshal: marshaler.New(instance),
}
}
type failureStore struct {
getErr error
setErr error
setCalls atomic.Int32
}
func (s *failureStore) Get(context.Context, any) (any, error) {
return nil, s.getErr
}
func (s *failureStore) GetWithTTL(context.Context, any) (any, time.Duration, error) {
return nil, 0, s.getErr
}
func (s *failureStore) Set(context.Context, any, any, ...store.Option) error {
s.setCalls.Add(1)
return s.setErr
}
func (*failureStore) Delete(context.Context, any) error {
return nil
}
func (*failureStore) Invalidate(context.Context, ...store.InvalidateOption) error {
return nil
}
func (*failureStore) Clear(context.Context) error {
return nil
}
func (*failureStore) GetType() string {
return "failure"
}
+1 -1
View File
@@ -42,7 +42,7 @@ func GreenText(conf GreenTextConf) (bool, error) {
}
if data.Code != 200 {
return false, fmt.Errorf(data.Msg)
return false, fmt.Errorf("%s", data.Msg)
}
if len(data.Data) < 1 || len(data.Data[0].Results) < 1 {
return false, fmt.Errorf("not expected response: %s", string(body))
+600 -2
View File
@@ -5,7 +5,605 @@ package config
import "github.com/artalkjs/artalk/v2/internal/config/meta"
// Cache result of `meta.GetEnvPathMap(config.Config{}, config.Template("en"))`
var EnvPathMapCache = map[string]string{"ATK_ADMIN_NOTIFY_BARK_ENABLED":"admin_notify.bark.enabled", "ATK_ADMIN_NOTIFY_BARK_SERVER":"admin_notify.bark.server", "ATK_ADMIN_NOTIFY_DING_TALK_ENABLED":"admin_notify.ding_talk.enabled", "ATK_ADMIN_NOTIFY_DING_TALK_SECRET":"admin_notify.ding_talk.secret", "ATK_ADMIN_NOTIFY_DING_TALK_TOKEN":"admin_notify.ding_talk.token", "ATK_ADMIN_NOTIFY_EMAIL":"admin_notify.email", "ATK_ADMIN_NOTIFY_EMAIL_ENABLED":"admin_notify.email.enabled", "ATK_ADMIN_NOTIFY_EMAIL_MAIL_SUBJECT":"admin_notify.email.mail_subject", "ATK_ADMIN_NOTIFY_EMAIL_MAIL_TPL":"admin_notify.email.mail_tpl", "ATK_ADMIN_NOTIFY_LARK_ENABLED":"admin_notify.lark.enabled", "ATK_ADMIN_NOTIFY_LARK_MSG_TYPE":"admin_notify.lark.msg_type", "ATK_ADMIN_NOTIFY_LARK_WEBHOOK_URL":"admin_notify.lark.webhook_url", "ATK_ADMIN_NOTIFY_LINE_CHANNEL_ACCESS_TOKEN":"admin_notify.line.channel_access_token", "ATK_ADMIN_NOTIFY_LINE_CHANNEL_SECRET":"admin_notify.line.channel_secret", "ATK_ADMIN_NOTIFY_LINE_ENABLED":"admin_notify.line.enabled", "ATK_ADMIN_NOTIFY_LINE_RECEIVERS":"admin_notify.line.receivers", "ATK_ADMIN_NOTIFY_LINE_RECEIVERS_$$":"admin_notify.line.receivers.$$", "ATK_ADMIN_NOTIFY_NOISE_MODE":"admin_notify.noise_mode", "ATK_ADMIN_NOTIFY_NOTIFY_PENDING":"admin_notify.notify_pending", "ATK_ADMIN_NOTIFY_NOTIFY_SUBJECT":"admin_notify.notify_subject", "ATK_ADMIN_NOTIFY_NOTIFY_TPL":"admin_notify.notify_tpl", "ATK_ADMIN_NOTIFY_SLACK_ENABLED":"admin_notify.slack.enabled", "ATK_ADMIN_NOTIFY_SLACK_OAUTH_TOKEN":"admin_notify.slack.oauth_token", "ATK_ADMIN_NOTIFY_SLACK_RECEIVERS":"admin_notify.slack.receivers", "ATK_ADMIN_NOTIFY_SLACK_RECEIVERS_$$":"admin_notify.slack.receivers.$$", "ATK_ADMIN_NOTIFY_TELEGRAM_API_TOKEN":"admin_notify.telegram.api_token", "ATK_ADMIN_NOTIFY_TELEGRAM_ENABLED":"admin_notify.telegram.enabled", "ATK_ADMIN_NOTIFY_TELEGRAM_RECEIVERS":"admin_notify.telegram.receivers", "ATK_ADMIN_NOTIFY_TELEGRAM_RECEIVERS_$$":"admin_notify.telegram.receivers.$$", "ATK_ADMIN_NOTIFY_WEBHOOK_ENABLED":"admin_notify.webhook.enabled", "ATK_ADMIN_NOTIFY_WEBHOOK_URL":"admin_notify.webhook.url", "ATK_ADMIN_USERS_$$_BADGE_COLOR":"admin_users.$$.badge_color", "ATK_ADMIN_USERS_$$_BADGE_NAME":"admin_users.$$.badge_name", "ATK_ADMIN_USERS_$$_EMAIL":"admin_users.$$.email", "ATK_ADMIN_USERS_$$_LINK":"admin_users.$$.link", "ATK_ADMIN_USERS_$$_NAME":"admin_users.$$.name", "ATK_ADMIN_USERS_$$_PASSWORD":"admin_users.$$.password", "ATK_ADMIN_USERS_$$_RECEIVE_EMAIL":"admin_users.$$.receive_email", "ATK_ADMIN__NOTIFY_BARK_ENABLED":"admin_notify.bark.enabled", "ATK_ADMIN__NOTIFY_BARK_SERVER":"admin_notify.bark.server", "ATK_ADMIN__NOTIFY_DING__TALK_ENABLED":"admin_notify.ding_talk.enabled", "ATK_ADMIN__NOTIFY_DING__TALK_SECRET":"admin_notify.ding_talk.secret", "ATK_ADMIN__NOTIFY_DING__TALK_TOKEN":"admin_notify.ding_talk.token", "ATK_ADMIN__NOTIFY_EMAIL":"admin_notify.email", "ATK_ADMIN__NOTIFY_LARK_ENABLED":"admin_notify.lark.enabled", "ATK_ADMIN__NOTIFY_LARK_MSG__TYPE":"admin_notify.lark.msg_type", "ATK_ADMIN__NOTIFY_LARK_WEBHOOK__URL":"admin_notify.lark.webhook_url", "ATK_ADMIN__NOTIFY_LINE_CHANNEL__ACCESS__TOKEN":"admin_notify.line.channel_access_token", "ATK_ADMIN__NOTIFY_LINE_CHANNEL__SECRET":"admin_notify.line.channel_secret", "ATK_ADMIN__NOTIFY_LINE_ENABLED":"admin_notify.line.enabled", "ATK_ADMIN__NOTIFY_LINE_RECEIVERS_$$":"admin_notify.line.receivers.$$", "ATK_ADMIN__NOTIFY_NOISE__MODE":"admin_notify.noise_mode", "ATK_ADMIN__NOTIFY_NOTIFY__PENDING":"admin_notify.notify_pending", "ATK_ADMIN__NOTIFY_NOTIFY__SUBJECT":"admin_notify.notify_subject", "ATK_ADMIN__NOTIFY_NOTIFY__TPL":"admin_notify.notify_tpl", "ATK_ADMIN__NOTIFY_SLACK_ENABLED":"admin_notify.slack.enabled", "ATK_ADMIN__NOTIFY_SLACK_OAUTH__TOKEN":"admin_notify.slack.oauth_token", "ATK_ADMIN__NOTIFY_SLACK_RECEIVERS_$$":"admin_notify.slack.receivers.$$", "ATK_ADMIN__NOTIFY_TELEGRAM_API__TOKEN":"admin_notify.telegram.api_token", "ATK_ADMIN__NOTIFY_TELEGRAM_ENABLED":"admin_notify.telegram.enabled", "ATK_ADMIN__NOTIFY_TELEGRAM_RECEIVERS_$$":"admin_notify.telegram.receivers.$$", "ATK_ADMIN__NOTIFY_WEBHOOK_ENABLED":"admin_notify.webhook.enabled", "ATK_ADMIN__NOTIFY_WEBHOOK_URL":"admin_notify.webhook.url", "ATK_ADMIN__USERS_$$_BADGE__COLOR":"admin_users.$$.badge_color", "ATK_ADMIN__USERS_$$_BADGE__NAME":"admin_users.$$.badge_name", "ATK_ADMIN__USERS_$$_EMAIL":"admin_users.$$.email", "ATK_ADMIN__USERS_$$_LINK":"admin_users.$$.link", "ATK_ADMIN__USERS_$$_NAME":"admin_users.$$.name", "ATK_ADMIN__USERS_$$_PASSWORD":"admin_users.$$.password", "ATK_ADMIN__USERS_$$_RECEIVE__EMAIL":"admin_users.$$.receive_email", "ATK_APP_KEY":"app_key", "ATK_APP__KEY":"app_key", "ATK_AUTH_ANONYMOUS":"auth.anonymous", "ATK_AUTH_APPLE_CLIENT_ID":"auth.apple.client_id", "ATK_AUTH_APPLE_CLIENT_SECRET":"auth.apple.client_secret", "ATK_AUTH_APPLE_CLIENT__ID":"auth.apple.client_id", "ATK_AUTH_APPLE_CLIENT__SECRET":"auth.apple.client_secret", "ATK_AUTH_APPLE_ENABLED":"auth.apple.enabled", "ATK_AUTH_AUTH0_CLIENT_ID":"auth.Line truncated
var EnvPathMapCache = map[string]string{
"ATK_ADMIN_NOTIFY_BARK_ENABLED": "admin_notify.bark.enabled",
"ATK_ADMIN_NOTIFY_BARK_SERVER": "admin_notify.bark.server",
"ATK_ADMIN_NOTIFY_DING_TALK_ENABLED": "admin_notify.ding_talk.enabled",
"ATK_ADMIN_NOTIFY_DING_TALK_SECRET": "admin_notify.ding_talk.secret",
"ATK_ADMIN_NOTIFY_DING_TALK_TOKEN": "admin_notify.ding_talk.token",
"ATK_ADMIN_NOTIFY_EMAIL": "admin_notify.email",
"ATK_ADMIN_NOTIFY_EMAIL_ENABLED": "admin_notify.email.enabled",
"ATK_ADMIN_NOTIFY_EMAIL_MAIL_SUBJECT": "admin_notify.email.mail_subject",
"ATK_ADMIN_NOTIFY_EMAIL_MAIL_TPL": "admin_notify.email.mail_tpl",
"ATK_ADMIN_NOTIFY_LARK_ENABLED": "admin_notify.lark.enabled",
"ATK_ADMIN_NOTIFY_LARK_MSG_TYPE": "admin_notify.lark.msg_type",
"ATK_ADMIN_NOTIFY_LARK_WEBHOOK_URL": "admin_notify.lark.webhook_url",
"ATK_ADMIN_NOTIFY_LINE_CHANNEL_ACCESS_TOKEN": "admin_notify.line.channel_access_token",
"ATK_ADMIN_NOTIFY_LINE_CHANNEL_SECRET": "admin_notify.line.channel_secret",
"ATK_ADMIN_NOTIFY_LINE_ENABLED": "admin_notify.line.enabled",
"ATK_ADMIN_NOTIFY_LINE_RECEIVERS": "admin_notify.line.receivers",
"ATK_ADMIN_NOTIFY_LINE_RECEIVERS_$$": "admin_notify.line.receivers.$$",
"ATK_ADMIN_NOTIFY_NOISE_MODE": "admin_notify.noise_mode",
"ATK_ADMIN_NOTIFY_NOTIFY_PENDING": "admin_notify.notify_pending",
"ATK_ADMIN_NOTIFY_NOTIFY_SUBJECT": "admin_notify.notify_subject",
"ATK_ADMIN_NOTIFY_NOTIFY_TPL": "admin_notify.notify_tpl",
"ATK_ADMIN_NOTIFY_SLACK_ENABLED": "admin_notify.slack.enabled",
"ATK_ADMIN_NOTIFY_SLACK_OAUTH_TOKEN": "admin_notify.slack.oauth_token",
"ATK_ADMIN_NOTIFY_SLACK_RECEIVERS": "admin_notify.slack.receivers",
"ATK_ADMIN_NOTIFY_SLACK_RECEIVERS_$$": "admin_notify.slack.receivers.$$",
"ATK_ADMIN_NOTIFY_TELEGRAM_API_TOKEN": "admin_notify.telegram.api_token",
"ATK_ADMIN_NOTIFY_TELEGRAM_ENABLED": "admin_notify.telegram.enabled",
"ATK_ADMIN_NOTIFY_TELEGRAM_RECEIVERS": "admin_notify.telegram.receivers",
"ATK_ADMIN_NOTIFY_TELEGRAM_RECEIVERS_$$": "admin_notify.telegram.receivers.$$",
"ATK_ADMIN_NOTIFY_WEBHOOK_ENABLED": "admin_notify.webhook.enabled",
"ATK_ADMIN_NOTIFY_WEBHOOK_URL": "admin_notify.webhook.url",
"ATK_ADMIN_USERS_$$_BADGE_COLOR": "admin_users.$$.badge_color",
"ATK_ADMIN_USERS_$$_BADGE_NAME": "admin_users.$$.badge_name",
"ATK_ADMIN_USERS_$$_EMAIL": "admin_users.$$.email",
"ATK_ADMIN_USERS_$$_LINK": "admin_users.$$.link",
"ATK_ADMIN_USERS_$$_NAME": "admin_users.$$.name",
"ATK_ADMIN_USERS_$$_PASSWORD": "admin_users.$$.password",
"ATK_ADMIN_USERS_$$_RECEIVE_EMAIL": "admin_users.$$.receive_email",
"ATK_ADMIN__NOTIFY_BARK_ENABLED": "admin_notify.bark.enabled",
"ATK_ADMIN__NOTIFY_BARK_SERVER": "admin_notify.bark.server",
"ATK_ADMIN__NOTIFY_DING__TALK_ENABLED": "admin_notify.ding_talk.enabled",
"ATK_ADMIN__NOTIFY_DING__TALK_SECRET": "admin_notify.ding_talk.secret",
"ATK_ADMIN__NOTIFY_DING__TALK_TOKEN": "admin_notify.ding_talk.token",
"ATK_ADMIN__NOTIFY_EMAIL": "admin_notify.email",
"ATK_ADMIN__NOTIFY_LARK_ENABLED": "admin_notify.lark.enabled",
"ATK_ADMIN__NOTIFY_LARK_MSG__TYPE": "admin_notify.lark.msg_type",
"ATK_ADMIN__NOTIFY_LARK_WEBHOOK__URL": "admin_notify.lark.webhook_url",
"ATK_ADMIN__NOTIFY_LINE_CHANNEL__ACCESS__TOKEN": "admin_notify.line.channel_access_token",
"ATK_ADMIN__NOTIFY_LINE_CHANNEL__SECRET": "admin_notify.line.channel_secret",
"ATK_ADMIN__NOTIFY_LINE_ENABLED": "admin_notify.line.enabled",
"ATK_ADMIN__NOTIFY_LINE_RECEIVERS_$$": "admin_notify.line.receivers.$$",
"ATK_ADMIN__NOTIFY_NOISE__MODE": "admin_notify.noise_mode",
"ATK_ADMIN__NOTIFY_NOTIFY__PENDING": "admin_notify.notify_pending",
"ATK_ADMIN__NOTIFY_NOTIFY__SUBJECT": "admin_notify.notify_subject",
"ATK_ADMIN__NOTIFY_NOTIFY__TPL": "admin_notify.notify_tpl",
"ATK_ADMIN__NOTIFY_SLACK_ENABLED": "admin_notify.slack.enabled",
"ATK_ADMIN__NOTIFY_SLACK_OAUTH__TOKEN": "admin_notify.slack.oauth_token",
"ATK_ADMIN__NOTIFY_SLACK_RECEIVERS_$$": "admin_notify.slack.receivers.$$",
"ATK_ADMIN__NOTIFY_TELEGRAM_API__TOKEN": "admin_notify.telegram.api_token",
"ATK_ADMIN__NOTIFY_TELEGRAM_ENABLED": "admin_notify.telegram.enabled",
"ATK_ADMIN__NOTIFY_TELEGRAM_RECEIVERS_$$": "admin_notify.telegram.receivers.$$",
"ATK_ADMIN__NOTIFY_WEBHOOK_ENABLED": "admin_notify.webhook.enabled",
"ATK_ADMIN__NOTIFY_WEBHOOK_URL": "admin_notify.webhook.url",
"ATK_ADMIN__USERS_$$_BADGE__COLOR": "admin_users.$$.badge_color",
"ATK_ADMIN__USERS_$$_BADGE__NAME": "admin_users.$$.badge_name",
"ATK_ADMIN__USERS_$$_EMAIL": "admin_users.$$.email",
"ATK_ADMIN__USERS_$$_LINK": "admin_users.$$.link",
"ATK_ADMIN__USERS_$$_NAME": "admin_users.$$.name",
"ATK_ADMIN__USERS_$$_PASSWORD": "admin_users.$$.password",
"ATK_ADMIN__USERS_$$_RECEIVE__EMAIL": "admin_users.$$.receive_email",
"ATK_APP_KEY": "app_key",
"ATK_APP__KEY": "app_key",
"ATK_AUTH_ANONYMOUS": "auth.anonymous",
"ATK_AUTH_APPLE_CLIENT_ID": "auth.apple.client_id",
"ATK_AUTH_APPLE_CLIENT_SECRET": "auth.apple.client_secret",
"ATK_AUTH_APPLE_CLIENT__ID": "auth.apple.client_id",
"ATK_AUTH_APPLE_CLIENT__SECRET": "auth.apple.client_secret",
"ATK_AUTH_APPLE_ENABLED": "auth.apple.enabled",
"ATK_AUTH_AUTH0_CLIENT_ID": "auth.auth0.client_id",
"ATK_AUTH_AUTH0_CLIENT_SECRET": "auth.auth0.client_secret",
"ATK_AUTH_AUTH0_CLIENT__ID": "auth.auth0.client_id",
"ATK_AUTH_AUTH0_CLIENT__SECRET": "auth.auth0.client_secret",
"ATK_AUTH_AUTH0_DOMAIN": "auth.auth0.domain",
"ATK_AUTH_AUTH0_ENABLED": "auth.auth0.enabled",
"ATK_AUTH_CALLBACK": "auth.callback",
"ATK_AUTH_DISCORD_CLIENT_ID": "auth.discord.client_id",
"ATK_AUTH_DISCORD_CLIENT_SECRET": "auth.discord.client_secret",
"ATK_AUTH_DISCORD_CLIENT__ID": "auth.discord.client_id",
"ATK_AUTH_DISCORD_CLIENT__SECRET": "auth.discord.client_secret",
"ATK_AUTH_DISCORD_ENABLED": "auth.discord.enabled",
"ATK_AUTH_EMAIL_ENABLED": "auth.email.enabled",
"ATK_AUTH_EMAIL_VERIFY_SUBJECT": "auth.email.verify_subject",
"ATK_AUTH_EMAIL_VERIFY_TPL": "auth.email.verify_tpl",
"ATK_AUTH_EMAIL_VERIFY__SUBJECT": "auth.email.verify_subject",
"ATK_AUTH_EMAIL_VERIFY__TPL": "auth.email.verify_tpl",
"ATK_AUTH_ENABLED": "auth.enabled",
"ATK_AUTH_FACEBOOK_CLIENT_ID": "auth.facebook.client_id",
"ATK_AUTH_FACEBOOK_CLIENT_SECRET": "auth.facebook.client_secret",
"ATK_AUTH_FACEBOOK_CLIENT__ID": "auth.facebook.client_id",
"ATK_AUTH_FACEBOOK_CLIENT__SECRET": "auth.facebook.client_secret",
"ATK_AUTH_FACEBOOK_ENABLED": "auth.facebook.enabled",
"ATK_AUTH_GITEA_CLIENT_ID": "auth.gitea.client_id",
"ATK_AUTH_GITEA_CLIENT_SECRET": "auth.gitea.client_secret",
"ATK_AUTH_GITEA_CLIENT__ID": "auth.gitea.client_id",
"ATK_AUTH_GITEA_CLIENT__SECRET": "auth.gitea.client_secret",
"ATK_AUTH_GITEA_ENABLED": "auth.gitea.enabled",
"ATK_AUTH_GITHUB_CLIENT_ID": "auth.github.client_id",
"ATK_AUTH_GITHUB_CLIENT_SECRET": "auth.github.client_secret",
"ATK_AUTH_GITHUB_CLIENT__ID": "auth.github.client_id",
"ATK_AUTH_GITHUB_CLIENT__SECRET": "auth.github.client_secret",
"ATK_AUTH_GITHUB_ENABLED": "auth.github.enabled",
"ATK_AUTH_GITLAB_CLIENT_ID": "auth.gitlab.client_id",
"ATK_AUTH_GITLAB_CLIENT_SECRET": "auth.gitlab.client_secret",
"ATK_AUTH_GITLAB_CLIENT__ID": "auth.gitlab.client_id",
"ATK_AUTH_GITLAB_CLIENT__SECRET": "auth.gitlab.client_secret",
"ATK_AUTH_GITLAB_ENABLED": "auth.gitlab.enabled",
"ATK_AUTH_GOOGLE_CLIENT_ID": "auth.google.client_id",
"ATK_AUTH_GOOGLE_CLIENT_SECRET": "auth.google.client_secret",
"ATK_AUTH_GOOGLE_CLIENT__ID": "auth.google.client_id",
"ATK_AUTH_GOOGLE_CLIENT__SECRET": "auth.google.client_secret",
"ATK_AUTH_GOOGLE_ENABLED": "auth.google.enabled",
"ATK_AUTH_LINE_CLIENT_ID": "auth.line.client_id",
"ATK_AUTH_LINE_CLIENT_SECRET": "auth.line.client_secret",
"ATK_AUTH_LINE_CLIENT__ID": "auth.line.client_id",
"ATK_AUTH_LINE_CLIENT__SECRET": "auth.line.client_secret",
"ATK_AUTH_LINE_ENABLED": "auth.line.enabled",
"ATK_AUTH_MASTODON_CLIENT_ID": "auth.mastodon.client_id",
"ATK_AUTH_MASTODON_CLIENT_SECRET": "auth.mastodon.client_secret",
"ATK_AUTH_MASTODON_CLIENT__ID": "auth.mastodon.client_id",
"ATK_AUTH_MASTODON_CLIENT__SECRET": "auth.mastodon.client_secret",
"ATK_AUTH_MASTODON_ENABLED": "auth.mastodon.enabled",
"ATK_AUTH_MICROSOFT_CLIENT_ID": "auth.microsoft.client_id",
"ATK_AUTH_MICROSOFT_CLIENT_SECRET": "auth.microsoft.client_secret",
"ATK_AUTH_MICROSOFT_CLIENT__ID": "auth.microsoft.client_id",
"ATK_AUTH_MICROSOFT_CLIENT__SECRET": "auth.microsoft.client_secret",
"ATK_AUTH_MICROSOFT_ENABLED": "auth.microsoft.enabled",
"ATK_AUTH_PATREON_CLIENT_ID": "auth.patreon.client_id",
"ATK_AUTH_PATREON_CLIENT_SECRET": "auth.patreon.client_secret",
"ATK_AUTH_PATREON_CLIENT__ID": "auth.patreon.client_id",
"ATK_AUTH_PATREON_CLIENT__SECRET": "auth.patreon.client_secret",
"ATK_AUTH_PATREON_ENABLED": "auth.patreon.enabled",
"ATK_AUTH_SLACK_CLIENT_ID": "auth.slack.client_id",
"ATK_AUTH_SLACK_CLIENT_SECRET": "auth.slack.client_secret",
"ATK_AUTH_SLACK_CLIENT__ID": "auth.slack.client_id",
"ATK_AUTH_SLACK_CLIENT__SECRET": "auth.slack.client_secret",
"ATK_AUTH_SLACK_ENABLED": "auth.slack.enabled",
"ATK_AUTH_SSO_ENABLED": "auth.sso.enabled",
"ATK_AUTH_SSO_ISSUER": "auth.sso.issuer",
"ATK_AUTH_STEAM_API_KEY": "auth.steam.api_key",
"ATK_AUTH_STEAM_API__KEY": "auth.steam.api_key",
"ATK_AUTH_STEAM_ENABLED": "auth.steam.enabled",
"ATK_AUTH_TIKTOK_CLIENT_ID": "auth.tiktok.client_id",
"ATK_AUTH_TIKTOK_CLIENT_SECRET": "auth.tiktok.client_secret",
"ATK_AUTH_TIKTOK_CLIENT__ID": "auth.tiktok.client_id",
"ATK_AUTH_TIKTOK_CLIENT__SECRET": "auth.tiktok.client_secret",
"ATK_AUTH_TIKTOK_ENABLED": "auth.tiktok.enabled",
"ATK_AUTH_TWITTER_CLIENT_ID": "auth.twitter.client_id",
"ATK_AUTH_TWITTER_CLIENT_SECRET": "auth.twitter.client_secret",
"ATK_AUTH_TWITTER_CLIENT__ID": "auth.twitter.client_id",
"ATK_AUTH_TWITTER_CLIENT__SECRET": "auth.twitter.client_secret",
"ATK_AUTH_TWITTER_ENABLED": "auth.twitter.enabled",
"ATK_AUTH_WECHAT_CLIENT_ID": "auth.wechat.client_id",
"ATK_AUTH_WECHAT_CLIENT_SECRET": "auth.wechat.client_secret",
"ATK_AUTH_WECHAT_CLIENT__ID": "auth.wechat.client_id",
"ATK_AUTH_WECHAT_CLIENT__SECRET": "auth.wechat.client_secret",
"ATK_AUTH_WECHAT_ENABLED": "auth.wechat.enabled",
"ATK_CACHE_ENABLED": "cache.enabled",
"ATK_CACHE_EXPIRES": "cache.expires",
"ATK_CACHE_REDIS_DB": "cache.redis.db",
"ATK_CACHE_REDIS_NETWORK": "cache.redis.network",
"ATK_CACHE_REDIS_PASSWORD": "cache.redis.password",
"ATK_CACHE_REDIS_USERNAME": "cache.redis.username",
"ATK_CACHE_SERVER": "cache.server",
"ATK_CACHE_TYPE": "cache.type",
"ATK_CACHE_WARM_UP": "cache.warm_up",
"ATK_CACHE_WARM__UP": "cache.warm_up",
"ATK_CAPTCHA_ACTION_LIMIT": "captcha.action_limit",
"ATK_CAPTCHA_ACTION_RESET": "captcha.action_reset",
"ATK_CAPTCHA_ACTION__LIMIT": "captcha.action_limit",
"ATK_CAPTCHA_ACTION__RESET": "captcha.action_reset",
"ATK_CAPTCHA_ALWAYS": "captcha.always",
"ATK_CAPTCHA_CAPTCHA_TYPE": "captcha.captcha_type",
"ATK_CAPTCHA_CAPTCHA__TYPE": "captcha.captcha_type",
"ATK_CAPTCHA_ENABLED": "captcha.enabled",
"ATK_CAPTCHA_GEETEST_CAPTCHA_ID": "captcha.geetest.captcha_id",
"ATK_CAPTCHA_GEETEST_CAPTCHA_KEY": "captcha.geetest.captcha_key",
"ATK_CAPTCHA_GEETEST_CAPTCHA__ID": "captcha.geetest.captcha_id",
"ATK_CAPTCHA_GEETEST_CAPTCHA__KEY": "captcha.geetest.captcha_key",
"ATK_CAPTCHA_HCAPTCHA_SECRET_KEY": "captcha.hcaptcha.secret_key",
"ATK_CAPTCHA_HCAPTCHA_SECRET__KEY": "captcha.hcaptcha.secret_key",
"ATK_CAPTCHA_HCAPTCHA_SITE_KEY": "captcha.hcaptcha.site_key",
"ATK_CAPTCHA_HCAPTCHA_SITE__KEY": "captcha.hcaptcha.site_key",
"ATK_CAPTCHA_RECAPTCHA_SECRET_KEY": "captcha.recaptcha.secret_key",
"ATK_CAPTCHA_RECAPTCHA_SECRET__KEY": "captcha.recaptcha.secret_key",
"ATK_CAPTCHA_RECAPTCHA_SITE_KEY": "captcha.recaptcha.site_key",
"ATK_CAPTCHA_RECAPTCHA_SITE__KEY": "captcha.recaptcha.site_key",
"ATK_CAPTCHA_TURNSTILE_SECRET_KEY": "captcha.turnstile.secret_key",
"ATK_CAPTCHA_TURNSTILE_SECRET__KEY": "captcha.turnstile.secret_key",
"ATK_CAPTCHA_TURNSTILE_SITE_KEY": "captcha.turnstile.site_key",
"ATK_CAPTCHA_TURNSTILE_SITE__KEY": "captcha.turnstile.site_key",
"ATK_DB_CHARSET": "db.charset",
"ATK_DB_CLIENT_CERT_PATH": "db.client_cert_path",
"ATK_DB_CLIENT_KEY_PATH": "db.client_key_path",
"ATK_DB_CLIENT__CERT__PATH": "db.client_cert_path",
"ATK_DB_CLIENT__KEY__PATH": "db.client_key_path",
"ATK_DB_DSN": "db.dsn",
"ATK_DB_FILE": "db.file",
"ATK_DB_HOST": "db.host",
"ATK_DB_NAME": "db.name",
"ATK_DB_PASSWORD": "db.password",
"ATK_DB_PORT": "db.port",
"ATK_DB_PREPARE_STMT": "db.prepare_stmt",
"ATK_DB_PREPARE__STMT": "db.prepare_stmt",
"ATK_DB_SERVER_CA_PATH": "db.server_ca_path",
"ATK_DB_SERVER__CA__PATH": "db.server_ca_path",
"ATK_DB_SSL": "db.ssl",
"ATK_DB_TABLE_PREFIX": "db.table_prefix",
"ATK_DB_TABLE__PREFIX": "db.table_prefix",
"ATK_DB_TYPE": "db.type",
"ATK_DB_USER": "db.user",
"ATK_DEBUG": "debug",
"ATK_EMAIL_ALI_DM_ACCESS_KEY_ID": "email.ali_dm.access_key_id",
"ATK_EMAIL_ALI_DM_ACCESS_KEY_SECRET": "email.ali_dm.access_key_secret",
"ATK_EMAIL_ALI_DM_ACCOUNT_NAME": "email.ali_dm.account_name",
"ATK_EMAIL_ALI_DM_REGION": "email.ali_dm.region",
"ATK_EMAIL_ALI__DM_ACCESS__KEY__ID": "email.ali_dm.access_key_id",
"ATK_EMAIL_ALI__DM_ACCESS__KEY__SECRET": "email.ali_dm.access_key_secret",
"ATK_EMAIL_ALI__DM_ACCOUNT__NAME": "email.ali_dm.account_name",
"ATK_EMAIL_ALI__DM_REGION": "email.ali_dm.region",
"ATK_EMAIL_ENABLED": "email.enabled",
"ATK_EMAIL_MAIL_SUBJECT": "email.mail_subject",
"ATK_EMAIL_MAIL_TPL": "email.mail_tpl",
"ATK_EMAIL_MAIL__SUBJECT": "email.mail_subject",
"ATK_EMAIL_MAIL__TPL": "email.mail_tpl",
"ATK_EMAIL_QUEUE_BUFFER_SIZE": "email.queue.buffer_size",
"ATK_EMAIL_QUEUE_BUFFER__SIZE": "email.queue.buffer_size",
"ATK_EMAIL_SEND_ADDR": "email.send_addr",
"ATK_EMAIL_SEND_NAME": "email.send_name",
"ATK_EMAIL_SEND_TYPE": "email.send_type",
"ATK_EMAIL_SEND__ADDR": "email.send_addr",
"ATK_EMAIL_SEND__NAME": "email.send_name",
"ATK_EMAIL_SEND__TYPE": "email.send_type",
"ATK_EMAIL_SMTP_FROM": "email.smtp.from",
"ATK_EMAIL_SMTP_HOST": "email.smtp.host",
"ATK_EMAIL_SMTP_PASSWORD": "email.smtp.password",
"ATK_EMAIL_SMTP_PORT": "email.smtp.port",
"ATK_EMAIL_SMTP_USERNAME": "email.smtp.username",
"ATK_FRONTEND": "frontend",
"ATK_FRONTEND_DARKMODE": "frontend.darkMode",
"ATK_FRONTEND_EDITORTRAVEL": "frontend.editorTravel",
"ATK_FRONTEND_EMOTICONS": "frontend.emoticons",
"ATK_FRONTEND_FLATMODE": "frontend.flatMode",
"ATK_FRONTEND_GRAVATAR_MIRROR": "frontend.gravatar.mirror",
"ATK_FRONTEND_GRAVATAR_PARAMS": "frontend.gravatar.params",
"ATK_FRONTEND_HEIGHTLIMIT_CHILDREN": "frontend.heightLimit.children",
"ATK_FRONTEND_HEIGHTLIMIT_CONTENT": "frontend.heightLimit.content",
"ATK_FRONTEND_HEIGHTLIMIT_SCROLLABLE": "frontend.heightLimit.scrollable",
"ATK_FRONTEND_IMGLAZYLOAD": "frontend.imgLazyLoad",
"ATK_FRONTEND_LISTSORT": "frontend.listSort",
"ATK_FRONTEND_NESTMAX": "frontend.nestMax",
"ATK_FRONTEND_NESTSORT": "frontend.nestSort",
"ATK_FRONTEND_NOCOMMENT": "frontend.noComment",
"ATK_FRONTEND_PAGINATION_AUTOLOAD": "frontend.pagination.autoLoad",
"ATK_FRONTEND_PAGINATION_PAGESIZE": "frontend.pagination.pageSize",
"ATK_FRONTEND_PAGINATION_READMORE": "frontend.pagination.readMore",
"ATK_FRONTEND_PLACEHOLDER": "frontend.placeholder",
"ATK_FRONTEND_PLUGINURLS": "frontend.pluginURLs",
"ATK_FRONTEND_PREVIEW": "frontend.preview",
"ATK_FRONTEND_REQTIMEOUT": "frontend.reqTimeout",
"ATK_FRONTEND_SENDBTN": "frontend.sendBtn",
"ATK_FRONTEND_UABADGE": "frontend.uaBadge",
"ATK_FRONTEND_VERSIONCHECK": "frontend.versionCheck",
"ATK_FRONTEND_VOTE": "frontend.vote",
"ATK_FRONTEND_VOTEDOWN": "frontend.voteDown",
"ATK_HOST": "host",
"ATK_HTTP_BODY_LIMIT": "http.body_limit",
"ATK_HTTP_BODY__LIMIT": "http.body_limit",
"ATK_HTTP_PROXY_HEADER": "http.proxy_header",
"ATK_HTTP_PROXY__HEADER": "http.proxy_header",
"ATK_IMG_UPLOAD_ENABLED": "img_upload.enabled",
"ATK_IMG_UPLOAD_MAX_SIZE": "img_upload.max_size",
"ATK_IMG_UPLOAD_PATH": "img_upload.path",
"ATK_IMG_UPLOAD_PUBLIC_PATH": "img_upload.public_path",
"ATK_IMG_UPLOAD_QUALITY": "img_upload.quality",
"ATK_IMG_UPLOAD_UPGIT_DEL_LOCAL": "img_upload.upgit.del_local",
"ATK_IMG_UPLOAD_UPGIT_ENABLED": "img_upload.upgit.enabled",
"ATK_IMG_UPLOAD_UPGIT_EXEC": "img_upload.upgit.exec",
"ATK_IMG__UPLOAD_ENABLED": "img_upload.enabled",
"ATK_IMG__UPLOAD_MAX__SIZE": "img_upload.max_size",
"ATK_IMG__UPLOAD_PATH": "img_upload.path",
"ATK_IMG__UPLOAD_PUBLIC__PATH": "img_upload.public_path",
"ATK_IMG__UPLOAD_QUALITY": "img_upload.quality",
"ATK_IMG__UPLOAD_UPGIT_DEL__LOCAL": "img_upload.upgit.del_local",
"ATK_IMG__UPLOAD_UPGIT_ENABLED": "img_upload.upgit.enabled",
"ATK_IMG__UPLOAD_UPGIT_EXEC": "img_upload.upgit.exec",
"ATK_IP_REGION_DB_PATH": "ip_region.db_path",
"ATK_IP_REGION_ENABLED": "ip_region.enabled",
"ATK_IP_REGION_PRECISION": "ip_region.precision",
"ATK_IP__REGION_DB__PATH": "ip_region.db_path",
"ATK_IP__REGION_ENABLED": "ip_region.enabled",
"ATK_IP__REGION_PRECISION": "ip_region.precision",
"ATK_LOCALE": "locale",
"ATK_LOGIN_TIMEOUT": "login_timeout",
"ATK_LOGIN__TIMEOUT": "login_timeout",
"ATK_LOG_ENABLED": "log.enabled",
"ATK_LOG_FILENAME": "log.filename",
"ATK_MODERATOR_AKISMET_KEY": "moderator.akismet_key",
"ATK_MODERATOR_AKISMET__KEY": "moderator.akismet_key",
"ATK_MODERATOR_ALIYUN_ACCESS_KEY_ID": "moderator.aliyun.access_key_id",
"ATK_MODERATOR_ALIYUN_ACCESS_KEY_SECRET": "moderator.aliyun.access_key_secret",
"ATK_MODERATOR_ALIYUN_ACCESS__KEY__ID": "moderator.aliyun.access_key_id",
"ATK_MODERATOR_ALIYUN_ACCESS__KEY__SECRET": "moderator.aliyun.access_key_secret",
"ATK_MODERATOR_ALIYUN_ENABLED": "moderator.aliyun.enabled",
"ATK_MODERATOR_ALIYUN_REGION": "moderator.aliyun.region",
"ATK_MODERATOR_API_FAIL_BLOCK": "moderator.api_fail_block",
"ATK_MODERATOR_API__FAIL__BLOCK": "moderator.api_fail_block",
"ATK_MODERATOR_KEYWORDS_ENABLED": "moderator.keywords.enabled",
"ATK_MODERATOR_KEYWORDS_FILES": "moderator.keywords.files",
"ATK_MODERATOR_KEYWORDS_FILES_$$": "moderator.keywords.files.$$",
"ATK_MODERATOR_KEYWORDS_FILE_SEP": "moderator.keywords.file_sep",
"ATK_MODERATOR_KEYWORDS_FILE__SEP": "moderator.keywords.file_sep",
"ATK_MODERATOR_KEYWORDS_PENDING": "moderator.keywords.pending",
"ATK_MODERATOR_KEYWORDS_REPLACE_TO": "moderator.keywords.replace_to",
"ATK_MODERATOR_KEYWORDS_REPLACE__TO": "moderator.keywords.replace_to",
"ATK_MODERATOR_PENDING_DEFAULT": "moderator.pending_default",
"ATK_MODERATOR_PENDING__DEFAULT": "moderator.pending_default",
"ATK_MODERATOR_TENCENT_ENABLED": "moderator.tencent.enabled",
"ATK_MODERATOR_TENCENT_REGION": "moderator.tencent.region",
"ATK_MODERATOR_TENCENT_SECRET_ID": "moderator.tencent.secret_id",
"ATK_MODERATOR_TENCENT_SECRET_KEY": "moderator.tencent.secret_key",
"ATK_MODERATOR_TENCENT_SECRET__ID": "moderator.tencent.secret_id",
"ATK_MODERATOR_TENCENT_SECRET__KEY": "moderator.tencent.secret_key",
"ATK_PORT": "port",
"ATK_SITE_DEFAULT": "site_default",
"ATK_SITE_URL": "site_url",
"ATK_SITE__DEFAULT": "site_default",
"ATK_SITE__URL": "site_url",
"ATK_SSL_CERT_PATH": "ssl.cert_path",
"ATK_SSL_CERT__PATH": "ssl.cert_path",
"ATK_SSL_ENABLED": "ssl.enabled",
"ATK_SSL_KEY_PATH": "ssl.key_path",
"ATK_SSL_KEY__PATH": "ssl.key_path",
"ATK_TIMEZONE": "timezone",
"ATK_TRUSTED_DOMAINS": "trusted_domains",
"ATK_TRUSTED_DOMAINS_$$": "trusted_domains.$$",
"ATK_TRUSTED__DOMAINS_$$": "trusted_domains.$$",
}
// Cache result of `meta.GetOptionsMetaData(config.Template("en"))`
var OptionsMetaCache = []meta.OptionsMeta{meta.OptionsMeta{Title:"Multi-Push", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY", Path:"admin_notify", PathText:"Multi-Push", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Multi-Push"}, meta.OptionsMeta{Title:"Bark", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_BARK", Path:"admin_notify.bark", PathText:"Multi-Push > Bark", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Bark"}, meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_BARK_ENABLED", Path:"admin_notify.bark.enabled", PathText:"Multi-Push > Bark > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"}, meta.OptionsMeta{Title:"Server", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_BARK_SERVER", Path:"admin_notify.bark.server", PathText:"Multi-Push > Bark > Server", Default:"http://day.app/xxxxxxx/", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Server"}, meta.OptionsMeta{Title:"DingTalk", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_DING_TALK", Path:"admin_notify.ding_talk", PathText:"Multi-Push > DingTalk", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"DingTalk"}, meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_DING_TALK_ENABLED", Path:"admin_notify.ding_talk.enabled", PathText:"Multi-Push > DingTalk > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"}, meta.OptionsMeta{Title:"Secret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_DING_TALK_SECRET", Path:"admin_notify.ding_talk.secret", PathText:"Multi-Push > DingTalk > Secret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Secret"}, meta.OptionsMeta{Title:"Token", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_DING_TALK_TOKEN", Path:"admin_notify.ding_talk.token", PathText:"Multi-Push > DingTalk > Token", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Token"}, meta.OptionsMeta{Title:"Notify admin", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_EMAIL", Path:"admin_notify.email", PathText:"Multi-Push > Notify admin", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Notify admin"}, meta.OptionsMeta{Title:"Enable", Desc:"can be disabled when using other push methods", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_EMAIL_ENABLED", Path:"admin_notify.email.enabled", PathText:"Multi-Push > Notify admin > Enable", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable (can be disabled when using other push methods)"}, meta.OptionsMeta{Title:"Email subject", Desc:"email subject sent to admin", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_EMAIL_MAIL_SUBJECT", Path:"admin_notify.email.mail_subject", PathText:"Multi-Push > Notify admin > Email subject", Default:"[{{site_name}}] Post \"{{page_title}}\" has new a comment", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Email subject (email subject sent to admin)"}, meta.OptionsMeta{Title:"Admin email template file", Desc:"set to file path to use custom template", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_EMAIL_MAIL_TPL", Path:"admin_notify.email.mail_tpl", PathText:"Multi-Push > Notify admin > Admin email template file", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Admin email template file (set to file path to use custom template)"}, meta.OptionsMeta{Title:"Lark", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LARK", Path:"admin_notify.lark", PathText:"Multi-Push > Lark", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Lark"}, meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LARK_ENABLED", Path:"admin_notify.lark.enabled", PathText:"Multi-Push > Lark > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"}, meta.OptionsMeta{Title:"Message type", Desc:"", Type:"string", Options:[]string{"text", "card"}, Env:"ATK_ADMIN_NOTIFY_LARK_MSG_TYPE", Path:"admin_notify.lark.msg_type", PathText:"Multi-Push > Lark > Message type", Default:"text", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Message type [\"text\", \"card\"]"}, meta.OptionsMeta{Title:"WebhookUrl", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LARK_WEBHOOK_URL", Path:"admin_notify.lark.webhook_url", PathText:"Multi-Push > Lark > WebhookUrl", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"WebhookUrl"}, meta.OptionsMeta{Title:"LINE", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LINE", Path:"admin_noLine truncated
var OptionsMetaCache = []meta.OptionsMeta{
meta.OptionsMeta{Title:"Multi-Push", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY", Path:"admin_notify", PathText:"Multi-Push", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Multi-Push"},
meta.OptionsMeta{Title:"Bark", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_BARK", Path:"admin_notify.bark", PathText:"Multi-Push > Bark", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Bark"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_BARK_ENABLED", Path:"admin_notify.bark.enabled", PathText:"Multi-Push > Bark > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Server", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_BARK_SERVER", Path:"admin_notify.bark.server", PathText:"Multi-Push > Bark > Server", Default:"http://day.app/xxxxxxx/", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Server"},
meta.OptionsMeta{Title:"DingTalk", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_DING_TALK", Path:"admin_notify.ding_talk", PathText:"Multi-Push > DingTalk", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"DingTalk"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_DING_TALK_ENABLED", Path:"admin_notify.ding_talk.enabled", PathText:"Multi-Push > DingTalk > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Secret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_DING_TALK_SECRET", Path:"admin_notify.ding_talk.secret", PathText:"Multi-Push > DingTalk > Secret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Secret"},
meta.OptionsMeta{Title:"Token", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_DING_TALK_TOKEN", Path:"admin_notify.ding_talk.token", PathText:"Multi-Push > DingTalk > Token", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Token"},
meta.OptionsMeta{Title:"Notify admin", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_EMAIL", Path:"admin_notify.email", PathText:"Multi-Push > Notify admin", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Notify admin"},
meta.OptionsMeta{Title:"Enable", Desc:"can be disabled when using other push methods", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_EMAIL_ENABLED", Path:"admin_notify.email.enabled", PathText:"Multi-Push > Notify admin > Enable", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable (can be disabled when using other push methods)"},
meta.OptionsMeta{Title:"Email subject", Desc:"email subject sent to admin", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_EMAIL_MAIL_SUBJECT", Path:"admin_notify.email.mail_subject", PathText:"Multi-Push > Notify admin > Email subject", Default:"[{{site_name}}] Post \"{{page_title}}\" has new a comment", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Email subject (email subject sent to admin)"},
meta.OptionsMeta{Title:"Admin email template file", Desc:"set to file path to use custom template", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_EMAIL_MAIL_TPL", Path:"admin_notify.email.mail_tpl", PathText:"Multi-Push > Notify admin > Admin email template file", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Admin email template file (set to file path to use custom template)"},
meta.OptionsMeta{Title:"Lark", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LARK", Path:"admin_notify.lark", PathText:"Multi-Push > Lark", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Lark"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LARK_ENABLED", Path:"admin_notify.lark.enabled", PathText:"Multi-Push > Lark > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Message type", Desc:"", Type:"string", Options:[]string{"text", "card"}, Env:"ATK_ADMIN_NOTIFY_LARK_MSG_TYPE", Path:"admin_notify.lark.msg_type", PathText:"Multi-Push > Lark > Message type", Default:"text", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Message type [\"text\", \"card\"]"},
meta.OptionsMeta{Title:"WebhookUrl", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LARK_WEBHOOK_URL", Path:"admin_notify.lark.webhook_url", PathText:"Multi-Push > Lark > WebhookUrl", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"WebhookUrl"},
meta.OptionsMeta{Title:"LINE", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LINE", Path:"admin_notify.line", PathText:"Multi-Push > LINE", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"LINE"},
meta.OptionsMeta{Title:"ChannelAccessToken", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LINE_CHANNEL_ACCESS_TOKEN", Path:"admin_notify.line.channel_access_token", PathText:"Multi-Push > LINE > ChannelAccessToken", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ChannelAccessToken"},
meta.OptionsMeta{Title:"ChannelSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LINE_CHANNEL_SECRET", Path:"admin_notify.line.channel_secret", PathText:"Multi-Push > LINE > ChannelSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ChannelSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LINE_ENABLED", Path:"admin_notify.line.enabled", PathText:"Multi-Push > LINE > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Receivers", Desc:"", Type:"[]interface {}", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_LINE_RECEIVERS", Path:"admin_notify.line.receivers", PathText:"Multi-Push > LINE > Receivers", Default:[]interface {}{"USER_ID_1", "GROUP_ID_1"}, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Receivers"},
meta.OptionsMeta{Title:"Noise mode", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_NOISE_MODE", Path:"admin_notify.noise_mode", PathText:"Multi-Push > Noise mode", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Noise mode -- noise_mode is disabled by default. -- -- When this option is set to `false`, only messages sent to the administrator will be notified, -- -- such as \"user A\" replies to \"user B\", the communication between these two users will not be notified to the administrator. --"},
meta.OptionsMeta{Title:"Pending comment still send notification", Desc:"notifications are still sent when comments are intercepted", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_NOTIFY_PENDING", Path:"admin_notify.notify_pending", PathText:"Multi-Push > Pending comment still send notification", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Pending comment still send notification (notifications are still sent when comments are intercepted)"},
meta.OptionsMeta{Title:"Notification template", Desc:"set to file path to use custom template", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_NOTIFY_TPL", Path:"admin_notify.notify_tpl", PathText:"Multi-Push > Notification template", Default:"default", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Notification template (set to file path to use custom template)"},
meta.OptionsMeta{Title:"Slack", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_SLACK", Path:"admin_notify.slack", PathText:"Multi-Push > Slack", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Slack"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_SLACK_ENABLED", Path:"admin_notify.slack.enabled", PathText:"Multi-Push > Slack > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"OauthToken", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_SLACK_OAUTH_TOKEN", Path:"admin_notify.slack.oauth_token", PathText:"Multi-Push > Slack > OauthToken", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"OauthToken"},
meta.OptionsMeta{Title:"Receivers", Desc:"", Type:"[]interface {}", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_SLACK_RECEIVERS", Path:"admin_notify.slack.receivers", PathText:"Multi-Push > Slack > Receivers", Default:[]interface {}{"CHANNEL_ID"}, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Receivers"},
meta.OptionsMeta{Title:"Telegram", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_TELEGRAM", Path:"admin_notify.telegram", PathText:"Multi-Push > Telegram", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Telegram"},
meta.OptionsMeta{Title:"ApiToken", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_TELEGRAM_API_TOKEN", Path:"admin_notify.telegram.api_token", PathText:"Multi-Push > Telegram > ApiToken", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ApiToken"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_TELEGRAM_ENABLED", Path:"admin_notify.telegram.enabled", PathText:"Multi-Push > Telegram > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Receivers", Desc:"", Type:"[]interface {}", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_TELEGRAM_RECEIVERS", Path:"admin_notify.telegram.receivers", PathText:"Multi-Push > Telegram > Receivers", Default:[]interface {}{0x76adf1}, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Receivers"},
meta.OptionsMeta{Title:"WebHook", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_WEBHOOK", Path:"admin_notify.webhook", PathText:"Multi-Push > WebHook", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"WebHook"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_WEBHOOK_ENABLED", Path:"admin_notify.webhook.enabled", PathText:"Multi-Push > WebHook > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Url", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_ADMIN_NOTIFY_WEBHOOK_URL", Path:"admin_notify.webhook.url", PathText:"Multi-Push > WebHook > Url", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Url"},
meta.OptionsMeta{Title:"App Key", Desc:"for generation of JWT", Type:"string", Options:[]string(nil), Env:"ATK_APP_KEY", Path:"app_key", PathText:"App Key", Default:"", IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"App Key (for generation of JWT)"},
meta.OptionsMeta{Title:"Social Login", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH", Path:"auth", PathText:"Social Login", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Social Login"},
meta.OptionsMeta{Title:"Allow anonymous commenting", Desc:"Allow skipping verification, only fill in an anonymous nickname and email", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_ANONYMOUS", Path:"auth.anonymous", PathText:"Social Login > Allow anonymous commenting", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Allow anonymous commenting (Allow skipping verification, only fill in an anonymous nickname and email)"},
meta.OptionsMeta{Title:"Apple", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_APPLE", Path:"auth.apple", PathText:"Social Login > Apple", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Apple"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_APPLE_CLIENT_ID", Path:"auth.apple.client_id", PathText:"Social Login > Apple > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_APPLE_CLIENT_SECRET", Path:"auth.apple.client_secret", PathText:"Social Login > Apple > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_APPLE_ENABLED", Path:"auth.apple.enabled", PathText:"Social Login > Apple > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Auth0", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_AUTH0", Path:"auth.auth0", PathText:"Social Login > Auth0", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Auth0"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_AUTH0_CLIENT_ID", Path:"auth.auth0.client_id", PathText:"Social Login > Auth0 > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_AUTH0_CLIENT_SECRET", Path:"auth.auth0.client_secret", PathText:"Social Login > Auth0 > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Domain", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_AUTH0_DOMAIN", Path:"auth.auth0.domain", PathText:"Social Login > Auth0 > Domain", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Domain"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_AUTH0_ENABLED", Path:"auth.auth0.enabled", PathText:"Social Login > Auth0 > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Callback URL", Desc:"https://example.com/api/v2/auth/{provider}/callback", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_CALLBACK", Path:"auth.callback", PathText:"Social Login > Callback URL", Default:"http://localhost:23366/api/v2/auth/{provider}/callback", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Callback URL (https://example.com/api/v2/auth/{provider}/callback)"},
meta.OptionsMeta{Title:"Discord", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_DISCORD", Path:"auth.discord", PathText:"Social Login > Discord", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Discord"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_DISCORD_CLIENT_ID", Path:"auth.discord.client_id", PathText:"Social Login > Discord > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_DISCORD_CLIENT_SECRET", Path:"auth.discord.client_secret", PathText:"Social Login > Discord > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_DISCORD_ENABLED", Path:"auth.discord.enabled", PathText:"Social Login > Discord > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Email", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_EMAIL", Path:"auth.email", PathText:"Social Login > Email", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Email"},
meta.OptionsMeta{Title:"Enable email password login", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_EMAIL_ENABLED", Path:"auth.email.enabled", PathText:"Social Login > Email > Enable email password login", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable email password login"},
meta.OptionsMeta{Title:"Verification email subject", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_EMAIL_VERIFY_SUBJECT", Path:"auth.email.verify_subject", PathText:"Social Login > Email > Verification email subject", Default:"Your Code - {{code}}", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Verification email subject"},
meta.OptionsMeta{Title:"Verification email template", Desc:"set to file path to use custom template", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_EMAIL_VERIFY_TPL", Path:"auth.email.verify_tpl", PathText:"Social Login > Email > Verification email template", Default:"default", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Verification email template (set to file path to use custom template)"},
meta.OptionsMeta{Title:"Enable Social Login", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_ENABLED", Path:"auth.enabled", PathText:"Social Login > Enable Social Login", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable Social Login"},
meta.OptionsMeta{Title:"Facebook", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_FACEBOOK", Path:"auth.facebook", PathText:"Social Login > Facebook", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Facebook"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_FACEBOOK_CLIENT_ID", Path:"auth.facebook.client_id", PathText:"Social Login > Facebook > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_FACEBOOK_CLIENT_SECRET", Path:"auth.facebook.client_secret", PathText:"Social Login > Facebook > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_FACEBOOK_ENABLED", Path:"auth.facebook.enabled", PathText:"Social Login > Facebook > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Gitea", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_GITEA", Path:"auth.gitea", PathText:"Social Login > Gitea", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Gitea"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_GITEA_CLIENT_ID", Path:"auth.gitea.client_id", PathText:"Social Login > Gitea > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_GITEA_CLIENT_SECRET", Path:"auth.gitea.client_secret", PathText:"Social Login > Gitea > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_GITEA_ENABLED", Path:"auth.gitea.enabled", PathText:"Social Login > Gitea > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"GitHub", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_GITHUB", Path:"auth.github", PathText:"Social Login > GitHub", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"GitHub"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_GITHUB_CLIENT_ID", Path:"auth.github.client_id", PathText:"Social Login > GitHub > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_GITHUB_CLIENT_SECRET", Path:"auth.github.client_secret", PathText:"Social Login > GitHub > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_GITHUB_ENABLED", Path:"auth.github.enabled", PathText:"Social Login > GitHub > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"GitLab", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_GITLAB", Path:"auth.gitlab", PathText:"Social Login > GitLab", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"GitLab"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_GITLAB_CLIENT_ID", Path:"auth.gitlab.client_id", PathText:"Social Login > GitLab > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_GITLAB_CLIENT_SECRET", Path:"auth.gitlab.client_secret", PathText:"Social Login > GitLab > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_GITLAB_ENABLED", Path:"auth.gitlab.enabled", PathText:"Social Login > GitLab > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Google", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_GOOGLE", Path:"auth.google", PathText:"Social Login > Google", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Google"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_GOOGLE_CLIENT_ID", Path:"auth.google.client_id", PathText:"Social Login > Google > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_GOOGLE_CLIENT_SECRET", Path:"auth.google.client_secret", PathText:"Social Login > Google > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_GOOGLE_ENABLED", Path:"auth.google.enabled", PathText:"Social Login > Google > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Line", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_LINE", Path:"auth.line", PathText:"Social Login > Line", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Line"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_LINE_CLIENT_ID", Path:"auth.line.client_id", PathText:"Social Login > Line > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_LINE_CLIENT_SECRET", Path:"auth.line.client_secret", PathText:"Social Login > Line > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_LINE_ENABLED", Path:"auth.line.enabled", PathText:"Social Login > Line > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Mastodon", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_MASTODON", Path:"auth.mastodon", PathText:"Social Login > Mastodon", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Mastodon"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_MASTODON_CLIENT_ID", Path:"auth.mastodon.client_id", PathText:"Social Login > Mastodon > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_MASTODON_CLIENT_SECRET", Path:"auth.mastodon.client_secret", PathText:"Social Login > Mastodon > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_MASTODON_ENABLED", Path:"auth.mastodon.enabled", PathText:"Social Login > Mastodon > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Microsoft", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_MICROSOFT", Path:"auth.microsoft", PathText:"Social Login > Microsoft", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Microsoft"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_MICROSOFT_CLIENT_ID", Path:"auth.microsoft.client_id", PathText:"Social Login > Microsoft > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_MICROSOFT_CLIENT_SECRET", Path:"auth.microsoft.client_secret", PathText:"Social Login > Microsoft > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_MICROSOFT_ENABLED", Path:"auth.microsoft.enabled", PathText:"Social Login > Microsoft > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Patreon", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_PATREON", Path:"auth.patreon", PathText:"Social Login > Patreon", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Patreon"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_PATREON_CLIENT_ID", Path:"auth.patreon.client_id", PathText:"Social Login > Patreon > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_PATREON_CLIENT_SECRET", Path:"auth.patreon.client_secret", PathText:"Social Login > Patreon > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_PATREON_ENABLED", Path:"auth.patreon.enabled", PathText:"Social Login > Patreon > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Slack", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_SLACK", Path:"auth.slack", PathText:"Social Login > Slack", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Slack"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_SLACK_CLIENT_ID", Path:"auth.slack.client_id", PathText:"Social Login > Slack > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_SLACK_CLIENT_SECRET", Path:"auth.slack.client_secret", PathText:"Social Login > Slack > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_SLACK_ENABLED", Path:"auth.slack.enabled", PathText:"Social Login > Slack > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"SSO", Desc:"Exchange external OIDC access tokens for Artalk login sessions, verified via the IdP's /userinfo endpoint", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_SSO", Path:"auth.sso", PathText:"Social Login > SSO", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"SSO (Exchange external OIDC access tokens for Artalk login sessions, verified via the IdP's /userinfo endpoint) -- Use this when the embedding app already runs OIDC and you want Artalk to inherit that session without its own popup login UI. --"},
meta.OptionsMeta{Title:"Enable SSO token exchange", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_SSO_ENABLED", Path:"auth.sso.enabled", PathText:"Social Login > SSO > Enable SSO token exchange", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable SSO token exchange"},
meta.OptionsMeta{Title:"OIDC issuer URL", Desc:"e.g. \"tenant.auth0.com\" or \"https://tenant.auth0.com\"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_SSO_ISSUER", Path:"auth.sso.issuer", PathText:"Social Login > SSO > OIDC issuer URL", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"OIDC issuer URL (e.g. \"tenant.auth0.com\" or \"https://tenant.auth0.com\")"},
meta.OptionsMeta{Title:"Steam", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_STEAM", Path:"auth.steam", PathText:"Social Login > Steam", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Steam"},
meta.OptionsMeta{Title:"ApiKey", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_STEAM_API_KEY", Path:"auth.steam.api_key", PathText:"Social Login > Steam > ApiKey", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ApiKey"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_STEAM_ENABLED", Path:"auth.steam.enabled", PathText:"Social Login > Steam > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Tiktok", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_TIKTOK", Path:"auth.tiktok", PathText:"Social Login > Tiktok", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Tiktok"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_TIKTOK_CLIENT_ID", Path:"auth.tiktok.client_id", PathText:"Social Login > Tiktok > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_TIKTOK_CLIENT_SECRET", Path:"auth.tiktok.client_secret", PathText:"Social Login > Tiktok > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_TIKTOK_ENABLED", Path:"auth.tiktok.enabled", PathText:"Social Login > Tiktok > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Twitter", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_TWITTER", Path:"auth.twitter", PathText:"Social Login > Twitter", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Twitter"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_TWITTER_CLIENT_ID", Path:"auth.twitter.client_id", PathText:"Social Login > Twitter > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_TWITTER_CLIENT_SECRET", Path:"auth.twitter.client_secret", PathText:"Social Login > Twitter > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_TWITTER_ENABLED", Path:"auth.twitter.enabled", PathText:"Social Login > Twitter > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"WeChat", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_AUTH_WECHAT", Path:"auth.wechat", PathText:"Social Login > WeChat", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"WeChat"},
meta.OptionsMeta{Title:"ClientId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_WECHAT_CLIENT_ID", Path:"auth.wechat.client_id", PathText:"Social Login > WeChat > ClientId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientId"},
meta.OptionsMeta{Title:"ClientSecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_AUTH_WECHAT_CLIENT_SECRET", Path:"auth.wechat.client_secret", PathText:"Social Login > WeChat > ClientSecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ClientSecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_AUTH_WECHAT_ENABLED", Path:"auth.wechat.enabled", PathText:"Social Login > WeChat > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Cache", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_CACHE", Path:"cache", PathText:"Cache", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Cache"},
meta.OptionsMeta{Title:"Enable cache", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_CACHE_ENABLED", Path:"cache.enabled", PathText:"Cache > Enable cache", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable cache"},
meta.OptionsMeta{Title:"Cache expiration time", Desc:"in minutes", Type:"uint64", Options:[]string(nil), Env:"ATK_CACHE_EXPIRES", Path:"cache.expires", PathText:"Cache > Cache expiration time", Default:0x1e, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Cache expiration time (in minutes)"},
meta.OptionsMeta{Title:"Redis config", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_CACHE_REDIS", Path:"cache.redis", PathText:"Cache > Redis config", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Redis config"},
meta.OptionsMeta{Title:"Redis database number", Desc:"e.g. 0", Type:"uint64", Options:[]string(nil), Env:"ATK_CACHE_REDIS_DB", Path:"cache.redis.db", PathText:"Cache > Redis config > Redis database number", Default:0x0, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Redis database number (e.g. 0)"},
meta.OptionsMeta{Title:"Connection type", Desc:"", Type:"string", Options:[]string{"tcp", "unix"}, Env:"ATK_CACHE_REDIS_NETWORK", Path:"cache.redis.network", PathText:"Cache > Redis config > Connection type", Default:"tcp", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Connection type [\"tcp\", \"unix\"]"},
meta.OptionsMeta{Title:"Redis password", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CACHE_REDIS_PASSWORD", Path:"cache.redis.password", PathText:"Cache > Redis config > Redis password", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Redis password"},
meta.OptionsMeta{Title:"Redis username", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CACHE_REDIS_USERNAME", Path:"cache.redis.username", PathText:"Cache > Redis config > Redis username", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Redis username"},
meta.OptionsMeta{Title:"Cache server address", Desc:"e.g. \"localhost:6379\"", Type:"string", Options:[]string(nil), Env:"ATK_CACHE_SERVER", Path:"cache.server", PathText:"Cache > Cache server address", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"-- The following is not necessary for `builtin` cache -- Cache server address (e.g. \"localhost:6379\")"},
meta.OptionsMeta{Title:"Cache type", Desc:"", Type:"string", Options:[]string{"redis", "memcache", "builtin"}, Env:"ATK_CACHE_TYPE", Path:"cache.type", PathText:"Cache > Cache type", Default:"builtin", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Cache type [\"redis\", \"memcache\", \"builtin\"]"},
meta.OptionsMeta{Title:"Cache warm up", Desc:"warm up cache when program starts", Type:"bool", Options:[]string(nil), Env:"ATK_CACHE_WARM_UP", Path:"cache.warm_up", PathText:"Cache > Cache warm up", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Cache warm up (warm up cache when program starts)"},
meta.OptionsMeta{Title:"Captcha", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_CAPTCHA", Path:"captcha", PathText:"Captcha", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Captcha"},
meta.OptionsMeta{Title:"Action limit", Desc:"the number of actions required to activate captcha", Type:"uint64", Options:[]string(nil), Env:"ATK_CAPTCHA_ACTION_LIMIT", Path:"captcha.action_limit", PathText:"Captcha > Action limit", Default:0x3, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Action limit (the number of actions required to activate captcha)"},
meta.OptionsMeta{Title:"Reset Timeout", Desc:"timeout to reset action counter. unit: s, set to -1 to disable", Type:"uint64", Options:[]string(nil), Env:"ATK_CAPTCHA_ACTION_RESET", Path:"captcha.action_reset", PathText:"Captcha > Reset Timeout", Default:0x3c, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Reset Timeout (timeout to reset action counter. unit: s, set to -1 to disable)"},
meta.OptionsMeta{Title:"Captcha is required always", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_CAPTCHA_ALWAYS", Path:"captcha.always", PathText:"Captcha > Captcha is required always", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Captcha is required always"},
meta.OptionsMeta{Title:"Captcha type", Desc:"", Type:"string", Options:[]string{"image", "turnstile", "recaptcha", "hcaptcha", "geetest"}, Env:"ATK_CAPTCHA_CAPTCHA_TYPE", Path:"captcha.captcha_type", PathText:"Captcha > Captcha type", Default:"image", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Captcha type [\"image\", \"turnstile\", \"recaptcha\", \"hcaptcha\", \"geetest\"]"},
meta.OptionsMeta{Title:"Enable captcha", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_CAPTCHA_ENABLED", Path:"captcha.enabled", PathText:"Captcha > Enable captcha", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable captcha"},
meta.OptionsMeta{Title:"Geetest", Desc:"https://www.geetest.com", Type:"<nil>", Options:[]string(nil), Env:"ATK_CAPTCHA_GEETEST", Path:"captcha.geetest", PathText:"Captcha > Geetest", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Geetest (https://www.geetest.com)"},
meta.OptionsMeta{Title:"CaptchaId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CAPTCHA_GEETEST_CAPTCHA_ID", Path:"captcha.geetest.captcha_id", PathText:"Captcha > Geetest > CaptchaId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"CaptchaId"},
meta.OptionsMeta{Title:"CaptchaKey", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CAPTCHA_GEETEST_CAPTCHA_KEY", Path:"captcha.geetest.captcha_key", PathText:"Captcha > Geetest > CaptchaKey", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"CaptchaKey"},
meta.OptionsMeta{Title:"hCaptcha", Desc:"https://www.hcaptcha.com/", Type:"<nil>", Options:[]string(nil), Env:"ATK_CAPTCHA_HCAPTCHA", Path:"captcha.hcaptcha", PathText:"Captcha > hCaptcha", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"hCaptcha (https://www.hcaptcha.com/)"},
meta.OptionsMeta{Title:"SecretKey", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CAPTCHA_HCAPTCHA_SECRET_KEY", Path:"captcha.hcaptcha.secret_key", PathText:"Captcha > hCaptcha > SecretKey", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"SecretKey"},
meta.OptionsMeta{Title:"SiteKey", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CAPTCHA_HCAPTCHA_SITE_KEY", Path:"captcha.hcaptcha.site_key", PathText:"Captcha > hCaptcha > SiteKey", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"SiteKey"},
meta.OptionsMeta{Title:"reCaptcha", Desc:"https://www.google.com/recaptcha/about/", Type:"<nil>", Options:[]string(nil), Env:"ATK_CAPTCHA_RECAPTCHA", Path:"captcha.recaptcha", PathText:"Captcha > reCaptcha", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"reCaptcha (https://www.google.com/recaptcha/about/)"},
meta.OptionsMeta{Title:"SecretKey", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CAPTCHA_RECAPTCHA_SECRET_KEY", Path:"captcha.recaptcha.secret_key", PathText:"Captcha > reCaptcha > SecretKey", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"SecretKey"},
meta.OptionsMeta{Title:"SiteKey", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CAPTCHA_RECAPTCHA_SITE_KEY", Path:"captcha.recaptcha.site_key", PathText:"Captcha > reCaptcha > SiteKey", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"SiteKey"},
meta.OptionsMeta{Title:"Turnstile", Desc:"https://www.cloudflare.com/products/turnstile/", Type:"<nil>", Options:[]string(nil), Env:"ATK_CAPTCHA_TURNSTILE", Path:"captcha.turnstile", PathText:"Captcha > Turnstile", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Turnstile (https://www.cloudflare.com/products/turnstile/)"},
meta.OptionsMeta{Title:"SecretKey", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CAPTCHA_TURNSTILE_SECRET_KEY", Path:"captcha.turnstile.secret_key", PathText:"Captcha > Turnstile > SecretKey", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"SecretKey"},
meta.OptionsMeta{Title:"SiteKey", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_CAPTCHA_TURNSTILE_SITE_KEY", Path:"captcha.turnstile.site_key", PathText:"Captcha > Turnstile > SiteKey", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"SiteKey"},
meta.OptionsMeta{Title:"Database", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_DB", Path:"db", PathText:"Database", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Database"},
meta.OptionsMeta{Title:"Database charset", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_DB_CHARSET", Path:"db.charset", PathText:"Database > Database charset", Default:"utf8mb4", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Database charset"},
meta.OptionsMeta{Title:"Database file", Desc:"only for SQLite", Type:"string", Options:[]string(nil), Env:"ATK_DB_FILE", Path:"db.file", PathText:"Database > Database file", Default:"./data/artalk.db", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Database file (only for SQLite)"},
meta.OptionsMeta{Title:"Host address", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_DB_HOST", Path:"db.host", PathText:"Database > Host address", Default:"localhost", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Host address"},
meta.OptionsMeta{Title:"Database name", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_DB_NAME", Path:"db.name", PathText:"Database > Database name", Default:"artalk", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"-- The following is not necessary for SQLite -- Database name"},
meta.OptionsMeta{Title:"Database password", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_DB_PASSWORD", Path:"db.password", PathText:"Database > Database password", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Database password"},
meta.OptionsMeta{Title:"Host port", Desc:"", Type:"uint64", Options:[]string(nil), Env:"ATK_DB_PORT", Path:"db.port", PathText:"Database > Host port", Default:0xcea, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Host port"},
meta.OptionsMeta{Title:"Prepared Statement", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_DB_PREPARE_STMT", Path:"db.prepare_stmt", PathText:"Database > Prepared Statement", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Prepared Statement"},
meta.OptionsMeta{Title:"Enable SSL mode", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_DB_SSL", Path:"db.ssl", PathText:"Database > Enable SSL mode", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable SSL mode"},
meta.OptionsMeta{Title:"Table prefix", Desc:"e.g. \"atk_\"", Type:"string", Options:[]string(nil), Env:"ATK_DB_TABLE_PREFIX", Path:"db.table_prefix", PathText:"Database > Table prefix", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Table prefix (e.g. \"atk_\")"},
meta.OptionsMeta{Title:"Database type", Desc:"", Type:"string", Options:[]string{"sqlite", "mysql", "pgsql", "mssql"}, Env:"ATK_DB_TYPE", Path:"db.type", PathText:"Database > Database type", Default:"sqlite", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Database type [\"sqlite\", \"mysql\", \"pgsql\", \"mssql\"]"},
meta.OptionsMeta{Title:"Database user", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_DB_USER", Path:"db.user", PathText:"Database > Database user", Default:"root", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Database user"},
meta.OptionsMeta{Title:"Debug mode", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_DEBUG", Path:"debug", PathText:"Debug mode", Default:false, IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"Debug mode"},
meta.OptionsMeta{Title:"Email", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_EMAIL", Path:"email", PathText:"Email", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Email"},
meta.OptionsMeta{Title:"Aliyun mail push", Desc:"set send method to \"ali_dm\" to enable; see: https://help.aliyun.com/document_detail/29444.html", Type:"<nil>", Options:[]string(nil), Env:"ATK_EMAIL_ALI_DM", Path:"email.ali_dm", PathText:"Email > Aliyun mail push", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Aliyun mail push (set send method to \"ali_dm\" to enable; see: https://help.aliyun.com/document_detail/29444.html)"},
meta.OptionsMeta{Title:"AccessKeyId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_ALI_DM_ACCESS_KEY_ID", Path:"email.ali_dm.access_key_id", PathText:"Email > Aliyun mail push > AccessKeyId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"AccessKeyId"},
meta.OptionsMeta{Title:"AccessKeySecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_ALI_DM_ACCESS_KEY_SECRET", Path:"email.ali_dm.access_key_secret", PathText:"Email > Aliyun mail push > AccessKeySecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"AccessKeySecret"},
meta.OptionsMeta{Title:"AccountName", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_ALI_DM_ACCOUNT_NAME", Path:"email.ali_dm.account_name", PathText:"Email > Aliyun mail push > AccountName", Default:"noreply@example.com", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"AccountName"},
meta.OptionsMeta{Title:"Enable email notification", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_EMAIL_ENABLED", Path:"email.enabled", PathText:"Email > Enable email notification", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable email notification"},
meta.OptionsMeta{Title:"Email subject", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_MAIL_SUBJECT", Path:"email.mail_subject", PathText:"Email > Email subject", Default:"[{{site_name}}] You got a reply from @{{reply_nick}}", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Email subject"},
meta.OptionsMeta{Title:"Email template file", Desc:"set to file path to use custom template", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_MAIL_TPL", Path:"email.mail_tpl", PathText:"Email > Email template file", Default:"default", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Email template file (set to file path to use custom template)"},
meta.OptionsMeta{Title:"Email address of sender", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_SEND_ADDR", Path:"email.send_addr", PathText:"Email > Email address of sender", Default:"noreply@example.com", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Email address of sender"},
meta.OptionsMeta{Title:"Nick name of sender", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_SEND_NAME", Path:"email.send_name", PathText:"Email > Nick name of sender", Default:"{{reply_nick}}", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Nick name of sender"},
meta.OptionsMeta{Title:"Send method", Desc:"", Type:"string", Options:[]string{"smtp", "ali_dm", "sendmail"}, Env:"ATK_EMAIL_SEND_TYPE", Path:"email.send_type", PathText:"Email > Send method", Default:"smtp", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Send method [\"smtp\", \"ali_dm\", \"sendmail\"]"},
meta.OptionsMeta{Title:"SMTP send", Desc:"set send method to \"smtp\" to enable", Type:"<nil>", Options:[]string(nil), Env:"ATK_EMAIL_SMTP", Path:"email.smtp", PathText:"Email > SMTP send", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"SMTP send (set send method to \"smtp\" to enable)"},
meta.OptionsMeta{Title:"Email address of sender", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_SMTP_HOST", Path:"email.smtp.host", PathText:"Email > SMTP send > Email address of sender", Default:"smtp.qq.com", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Email address of sender"},
meta.OptionsMeta{Title:"Password", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_SMTP_PASSWORD", Path:"email.smtp.password", PathText:"Email > SMTP send > Password", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Password"},
meta.OptionsMeta{Title:"Email port", Desc:"", Type:"uint64", Options:[]string(nil), Env:"ATK_EMAIL_SMTP_PORT", Path:"email.smtp.port", PathText:"Email > SMTP send > Email port", Default:0x24b, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Email port"},
meta.OptionsMeta{Title:"Email address of sender", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_EMAIL_SMTP_USERNAME", Path:"email.smtp.username", PathText:"Email > SMTP send > Email address of sender", Default:"example@qq.com", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Email address of sender"},
meta.OptionsMeta{Title:"UI Settings", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_FRONTEND", Path:"frontend", PathText:"UI Settings", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"UI Settings"},
meta.OptionsMeta{Title:"Dark mode", Desc:"", Type:"string", Options:[]string{"inherit", "auto"}, Env:"ATK_FRONTEND_DARKMODE", Path:"frontend.darkMode", PathText:"UI Settings > Dark mode", Default:"inherit", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Dark mode [\"inherit\", \"auto\"]"},
meta.OptionsMeta{Title:"Movable comment box", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_EDITORTRAVEL", Path:"frontend.editorTravel", PathText:"UI Settings > Movable comment box", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Movable comment box"},
meta.OptionsMeta{Title:"Emoticons", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_FRONTEND_EMOTICONS", Path:"frontend.emoticons", PathText:"UI Settings > Emoticons", Default:"https://cdn.jsdelivr.net/gh/ArtalkJS/Emoticons/grps/default.json", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Emoticons"},
meta.OptionsMeta{Title:"Flatten mode", Desc:"", Type:"string", Options:[]string{"auto", "true", "false"}, Env:"ATK_FRONTEND_FLATMODE", Path:"frontend.flatMode", PathText:"UI Settings > Flatten mode", Default:"auto", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Flatten mode [\"auto\", true, false]"},
meta.OptionsMeta{Title:"Gravatar", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_FRONTEND_GRAVATAR", Path:"frontend.gravatar", PathText:"UI Settings > Gravatar", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Gravatar"},
meta.OptionsMeta{Title:"API URL", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_FRONTEND_GRAVATAR_MIRROR", Path:"frontend.gravatar.mirror", PathText:"UI Settings > Gravatar > API URL", Default:"https://www.gravatar.com/avatar/", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"API URL"},
meta.OptionsMeta{Title:"API parameters", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_FRONTEND_GRAVATAR_PARAMS", Path:"frontend.gravatar.params", PathText:"UI Settings > Gravatar > API parameters", Default:"sha256=1&d=mp&s=240", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"API parameters"},
meta.OptionsMeta{Title:"Content height limit", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_FRONTEND_HEIGHTLIMIT", Path:"frontend.heightLimit", PathText:"UI Settings > Content height limit", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Content height limit"},
meta.OptionsMeta{Title:"Sub-comment area height limit", Desc:"unit: px", Type:"uint64", Options:[]string(nil), Env:"ATK_FRONTEND_HEIGHTLIMIT_CHILDREN", Path:"frontend.heightLimit.children", PathText:"UI Settings > Content height limit > Sub-comment area height limit", Default:0x190, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Sub-comment area height limit (unit: px)"},
meta.OptionsMeta{Title:"Comment content height limit", Desc:"unit: px", Type:"uint64", Options:[]string(nil), Env:"ATK_FRONTEND_HEIGHTLIMIT_CONTENT", Path:"frontend.heightLimit.content", PathText:"UI Settings > Content height limit > Comment content height limit", Default:0x12c, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Comment content height limit (unit: px)"},
meta.OptionsMeta{Title:"Scrollable", Desc:"scrollable height limit area", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_HEIGHTLIMIT_SCROLLABLE", Path:"frontend.heightLimit.scrollable", PathText:"UI Settings > Content height limit > Scrollable", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Scrollable (scrollable height limit area)"},
meta.OptionsMeta{Title:"Image lazy load", Desc:"", Type:"bool", Options:[]string{"false", "native", "data-src"}, Env:"ATK_FRONTEND_IMGLAZYLOAD", Path:"frontend.imgLazyLoad", PathText:"UI Settings > Image lazy load", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Image lazy load [false, \"native\", \"data-src\"]"},
meta.OptionsMeta{Title:"Comment sorting", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_LISTSORT", Path:"frontend.listSort", PathText:"UI Settings > Comment sorting", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Comment sorting"},
meta.OptionsMeta{Title:"Maximum nesting level", Desc:"", Type:"uint64", Options:[]string(nil), Env:"ATK_FRONTEND_NESTMAX", Path:"frontend.nestMax", PathText:"UI Settings > Maximum nesting level", Default:0x2, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Maximum nesting level"},
meta.OptionsMeta{Title:"Nesting comment sorting rules", Desc:"", Type:"string", Options:[]string{"DATE_ASC", "DATE_DESC", "VOTE_UP_DESC"}, Env:"ATK_FRONTEND_NESTSORT", Path:"frontend.nestSort", PathText:"UI Settings > Nesting comment sorting rules", Default:"DATE_ASC", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Nesting comment sorting rules [\"DATE_ASC\", \"DATE_DESC\", \"VOTE_UP_DESC\"]"},
meta.OptionsMeta{Title:"Text to display when there is", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_FRONTEND_NOCOMMENT", Path:"frontend.noComment", PathText:"UI Settings > Text to display when there is", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Text to display when there is"},
meta.OptionsMeta{Title:"Comment pagination", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_FRONTEND_PAGINATION", Path:"frontend.pagination", PathText:"UI Settings > Comment pagination", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Comment pagination"},
meta.OptionsMeta{Title:"Scroll loading", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_PAGINATION_AUTOLOAD", Path:"frontend.pagination.autoLoad", PathText:"UI Settings > Comment pagination > Scroll loading", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Scroll loading"},
meta.OptionsMeta{Title:"Number of comments per page", Desc:"", Type:"uint64", Options:[]string(nil), Env:"ATK_FRONTEND_PAGINATION_PAGESIZE", Path:"frontend.pagination.pageSize", PathText:"UI Settings > Comment pagination > Number of comments per page", Default:0x14, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Number of comments per page"},
meta.OptionsMeta{Title:"Load more mode", Desc:"disabled to use pagination bar", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_PAGINATION_READMORE", Path:"frontend.pagination.readMore", PathText:"UI Settings > Comment pagination > Load more mode", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Load more mode (disabled to use pagination bar)"},
meta.OptionsMeta{Title:"Comment box placeholder", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_FRONTEND_PLACEHOLDER", Path:"frontend.placeholder", PathText:"UI Settings > Comment box placeholder", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Comment box placeholder"},
meta.OptionsMeta{Title:"Plugins", Desc:"", Type:"[]interface {}", Options:[]string(nil), Env:"ATK_FRONTEND_PLUGINURLS", Path:"frontend.pluginURLs", PathText:"UI Settings > Plugins", Default:[]interface {}{}, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Plugins"},
meta.OptionsMeta{Title:"Editor real-time preview", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_PREVIEW", Path:"frontend.preview", PathText:"UI Settings > Editor real-time preview", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Editor real-time preview"},
meta.OptionsMeta{Title:"Request timeout", Desc:"unit: ms", Type:"uint64", Options:[]string(nil), Env:"ATK_FRONTEND_REQTIMEOUT", Path:"frontend.reqTimeout", PathText:"UI Settings > Request timeout", Default:0x3a98, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Request timeout (unit: ms)"},
meta.OptionsMeta{Title:"Text of the send button", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_FRONTEND_SENDBTN", Path:"frontend.sendBtn", PathText:"UI Settings > Text of the send button", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Text of the send button"},
meta.OptionsMeta{Title:"User UA badge", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_UABADGE", Path:"frontend.uaBadge", PathText:"UI Settings > User UA badge", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"User UA badge"},
meta.OptionsMeta{Title:"Version check", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_VERSIONCHECK", Path:"frontend.versionCheck", PathText:"UI Settings > Version check", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Version check"},
meta.OptionsMeta{Title:"Vote button", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_VOTE", Path:"frontend.vote", PathText:"UI Settings > Vote button", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Vote button"},
meta.OptionsMeta{Title:"Vote down button", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_FRONTEND_VOTEDOWN", Path:"frontend.voteDown", PathText:"UI Settings > Vote down button", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Vote down button"},
meta.OptionsMeta{Title:"Listen host", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_HOST", Path:"host", PathText:"Listen host", Default:"0.0.0.0", IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"Listen host"},
meta.OptionsMeta{Title:"Web server", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_HTTP", Path:"http", PathText:"Web server", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Web server"},
meta.OptionsMeta{Title:"Body size limit", Desc:"unit: MB", Type:"uint64", Options:[]string(nil), Env:"ATK_HTTP_BODY_LIMIT", Path:"http.body_limit", PathText:"Web server > Body size limit", Default:0x64, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Body size limit (unit: MB)"},
meta.OptionsMeta{Title:"Proxy Header", Desc:"fill `X-Forwarded-For` to get user real IP if behind a trusted reverse proxy or CDN", Type:"string", Options:[]string(nil), Env:"ATK_HTTP_PROXY_HEADER", Path:"http.proxy_header", PathText:"Web server > Proxy Header", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Proxy Header (fill `X-Forwarded-For` to get user real IP if behind a trusted reverse proxy or CDN)"},
meta.OptionsMeta{Title:"Upload", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_IMG_UPLOAD", Path:"img_upload", PathText:"Upload", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Upload"},
meta.OptionsMeta{Title:"Enable image upload", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_IMG_UPLOAD_ENABLED", Path:"img_upload.enabled", PathText:"Upload > Enable image upload", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable image upload"},
meta.OptionsMeta{Title:"Image size limit", Desc:"unit: MB", Type:"uint64", Options:[]string(nil), Env:"ATK_IMG_UPLOAD_MAX_SIZE", Path:"img_upload.max_size", PathText:"Upload > Image size limit", Default:0x5, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Image size limit (unit: MB)"},
meta.OptionsMeta{Title:"Image storage", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_IMG_UPLOAD_PATH", Path:"img_upload.path", PathText:"Upload > Image storage", Default:"./data/artalk-img/", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Image storage"},
meta.OptionsMeta{Title:"Image link base path", Desc:"default: \"/static/images/\"", Type:"<nil>", Options:[]string(nil), Env:"ATK_IMG_UPLOAD_PUBLIC_PATH", Path:"img_upload.public_path", PathText:"Upload > Image link base path", Default:interface {}(nil), IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Image link base path (default: \"/static/images/\")"},
meta.OptionsMeta{Title:"Upgit config", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_IMG_UPLOAD_UPGIT", Path:"img_upload.upgit", PathText:"Upload > Upgit config", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Upgit config"},
meta.OptionsMeta{Title:"Delete local image after upload success", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_IMG_UPLOAD_UPGIT_DEL_LOCAL", Path:"img_upload.upgit.del_local", PathText:"Upload > Upgit config > Delete local image after upload success", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Delete local image after upload success"},
meta.OptionsMeta{Title:"Enable Upgit", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_IMG_UPLOAD_UPGIT_ENABLED", Path:"img_upload.upgit.enabled", PathText:"Upload > Upgit config > Enable Upgit", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable Upgit"},
meta.OptionsMeta{Title:"Command line arguments", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_IMG_UPLOAD_UPGIT_EXEC", Path:"img_upload.upgit.exec", PathText:"Upload > Upgit config > Command line arguments", Default:"upgit -c UPGIT_CONF_FILE_PATH -t /artalk-img", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Command line arguments"},
meta.OptionsMeta{Title:"Language", Desc:"follow Unicode BCP 47", Type:"string", Options:[]string{"en", "zh-CN", "zh-TW", "ja", "fr", "ko", "ru", "tr"}, Env:"ATK_LOCALE", Path:"locale", PathText:"Language", Default:"en", IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"Language (follow Unicode BCP 47) [\"en\", \"zh-CN\", \"zh-TW\", \"ja\", \"fr\", \"ko\", \"ru\", \"tr\"] -- see https://www.techonthenet.com/js/language_tags.php --"},
meta.OptionsMeta{Title:"Logging", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_LOG", Path:"log", PathText:"Logging", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Logging"},
meta.OptionsMeta{Title:"Login timeout", Desc:"in seconds", Type:"uint64", Options:[]string(nil), Env:"ATK_LOGIN_TIMEOUT", Path:"login_timeout", PathText:"Login timeout", Default:0x3f480, IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"Login timeout (in seconds)"},
meta.OptionsMeta{Title:"Enable logging", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_LOG_ENABLED", Path:"log.enabled", PathText:"Logging > Enable logging", Default:true, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable logging"},
meta.OptionsMeta{Title:"Log file path", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_LOG_FILENAME", Path:"log.filename", PathText:"Logging > Log file path", Default:"./data/artalk.log", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Log file path"},
meta.OptionsMeta{Title:"Moderator", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_MODERATOR", Path:"moderator", PathText:"Moderator", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"Moderator -- Comment examination before being public --"},
meta.OptionsMeta{Title:"Akismet Key", Desc:"Akismet anti-spam service, https://akismet.com", Type:"string", Options:[]string(nil), Env:"ATK_MODERATOR_AKISMET_KEY", Path:"moderator.akismet_key", PathText:"Moderator > Akismet Key", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Akismet Key (Akismet anti-spam service, https://akismet.com)"},
meta.OptionsMeta{Title:"Aliyun Content Security", Desc:"Auto review comments with Aliyun Content Security", Type:"<nil>", Options:[]string(nil), Env:"ATK_MODERATOR_ALIYUN", Path:"moderator.aliyun", PathText:"Moderator > Aliyun Content Security", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Aliyun Content Security (Auto review comments with Aliyun Content Security) -- see https://help.aliyun.com/document_detail/28417.html --"},
meta.OptionsMeta{Title:"AccessKeyId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_MODERATOR_ALIYUN_ACCESS_KEY_ID", Path:"moderator.aliyun.access_key_id", PathText:"Moderator > Aliyun Content Security > AccessKeyId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"AccessKeyId"},
meta.OptionsMeta{Title:"AccessKeySecret", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_MODERATOR_ALIYUN_ACCESS_KEY_SECRET", Path:"moderator.aliyun.access_key_secret", PathText:"Moderator > Aliyun Content Security > AccessKeySecret", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"AccessKeySecret"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_MODERATOR_ALIYUN_ENABLED", Path:"moderator.aliyun.enabled", PathText:"Moderator > Aliyun Content Security > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Region", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_MODERATOR_ALIYUN_REGION", Path:"moderator.aliyun.region", PathText:"Moderator > Aliyun Content Security > Region", Default:"cn-shanghai", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Region"},
meta.OptionsMeta{Title:"Block when API request fails", Desc:"set to false to let comments pass when API request fails", Type:"bool", Options:[]string(nil), Env:"ATK_MODERATOR_API_FAIL_BLOCK", Path:"moderator.api_fail_block", PathText:"Moderator > Block when API request fails", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Block when API request fails (set to false to let comments pass when API request fails)"},
meta.OptionsMeta{Title:"Keyword filter", Desc:"local offline dictionary", Type:"<nil>", Options:[]string(nil), Env:"ATK_MODERATOR_KEYWORDS", Path:"moderator.keywords", PathText:"Moderator > Keyword filter", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Keyword filter (local offline dictionary)"},
meta.OptionsMeta{Title:"Enable keyword filter", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_MODERATOR_KEYWORDS_ENABLED", Path:"moderator.keywords.enabled", PathText:"Moderator > Keyword filter > Enable keyword filter", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable keyword filter"},
meta.OptionsMeta{Title:"Dictionary file", Desc:"support multiple dictionary files", Type:"[]interface {}", Options:[]string(nil), Env:"ATK_MODERATOR_KEYWORDS_FILES", Path:"moderator.keywords.files", PathText:"Moderator > Keyword filter > Dictionary file", Default:[]interface {}{"./data/keywords_1.txt"}, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Dictionary file (support multiple dictionary files)"},
meta.OptionsMeta{Title:"FileSep", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_MODERATOR_KEYWORDS_FILE_SEP", Path:"moderator.keywords.file_sep", PathText:"Moderator > Keyword filter > FileSep", Default:"\n", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"FileSep"},
meta.OptionsMeta{Title:"Set to pending when match", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_MODERATOR_KEYWORDS_PENDING", Path:"moderator.keywords.pending", PathText:"Moderator > Keyword filter > Set to pending when match", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Set to pending when match"},
meta.OptionsMeta{Title:"ReplaceTo", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_MODERATOR_KEYWORDS_REPLACE_TO", Path:"moderator.keywords.replace_to", PathText:"Moderator > Keyword filter > ReplaceTo", Default:"x", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"ReplaceTo"},
meta.OptionsMeta{Title:"Default pending", Desc:"new comments need to be approved by admin", Type:"bool", Options:[]string(nil), Env:"ATK_MODERATOR_PENDING_DEFAULT", Path:"moderator.pending_default", PathText:"Moderator > Default pending", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Default pending (new comments need to be approved by admin)"},
meta.OptionsMeta{Title:"Tencent Cloud Content Security", Desc:"Auto review comments with Tencent Cloud Content Security", Type:"<nil>", Options:[]string(nil), Env:"ATK_MODERATOR_TENCENT", Path:"moderator.tencent", PathText:"Moderator > Tencent Cloud Content Security", Default:interface {}(nil), IsRoot:false, HasChild:true, AllowsSet:false, CommentRaw:"Tencent Cloud Content Security (Auto review comments with Tencent Cloud Content Security) -- see https://cloud.tencent.com/document/product/1124/64508 --"},
meta.OptionsMeta{Title:"Enabled", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_MODERATOR_TENCENT_ENABLED", Path:"moderator.tencent.enabled", PathText:"Moderator > Tencent Cloud Content Security > Enabled", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enabled"},
meta.OptionsMeta{Title:"Region", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_MODERATOR_TENCENT_REGION", Path:"moderator.tencent.region", PathText:"Moderator > Tencent Cloud Content Security > Region", Default:"ap-guangzhou", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Region"},
meta.OptionsMeta{Title:"SecretId", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_MODERATOR_TENCENT_SECRET_ID", Path:"moderator.tencent.secret_id", PathText:"Moderator > Tencent Cloud Content Security > SecretId", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"SecretId"},
meta.OptionsMeta{Title:"SecretKey", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_MODERATOR_TENCENT_SECRET_KEY", Path:"moderator.tencent.secret_key", PathText:"Moderator > Tencent Cloud Content Security > SecretKey", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"SecretKey"},
meta.OptionsMeta{Title:"Listen port", Desc:"", Type:"uint64", Options:[]string(nil), Env:"ATK_PORT", Path:"port", PathText:"Listen port", Default:0x5b46, IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"Listen port"},
meta.OptionsMeta{Title:"Default site name", Desc:"create when app is first launched", Type:"string", Options:[]string(nil), Env:"ATK_SITE_DEFAULT", Path:"site_default", PathText:"Default site name", Default:"Default Site", IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"Default site name (create when app is first launched)"},
meta.OptionsMeta{Title:"Default site url", Desc:"", Type:"string", Options:[]string(nil), Env:"ATK_SITE_URL", Path:"site_url", PathText:"Default site url", Default:"", IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"Default site url"},
meta.OptionsMeta{Title:"SSL", Desc:"", Type:"<nil>", Options:[]string(nil), Env:"ATK_SSL", Path:"ssl", PathText:"SSL", Default:interface {}(nil), IsRoot:true, HasChild:true, AllowsSet:false, CommentRaw:"SSL"},
meta.OptionsMeta{Title:"Certificate file path", Desc:"e.g. \"/etc/letsencrypt/live/example.com/fullchain.pem\"", Type:"string", Options:[]string(nil), Env:"ATK_SSL_CERT_PATH", Path:"ssl.cert_path", PathText:"SSL > Certificate file path", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Certificate file path (e.g. \"/etc/letsencrypt/live/example.com/fullchain.pem\")"},
meta.OptionsMeta{Title:"Enable SSL", Desc:"", Type:"bool", Options:[]string(nil), Env:"ATK_SSL_ENABLED", Path:"ssl.enabled", PathText:"SSL > Enable SSL", Default:false, IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Enable SSL"},
meta.OptionsMeta{Title:"Key file path", Desc:"e.g. \"/etc/letsencrypt/live/example.com/privkey.pem\"", Type:"string", Options:[]string(nil), Env:"ATK_SSL_KEY_PATH", Path:"ssl.key_path", PathText:"SSL > Key file path", Default:"", IsRoot:false, HasChild:false, AllowsSet:true, CommentRaw:"Key file path (e.g. \"/etc/letsencrypt/live/example.com/privkey.pem\")"},
meta.OptionsMeta{Title:"Timezone", Desc:"follow IANA Time Zone Database", Type:"string", Options:[]string(nil), Env:"ATK_TIMEZONE", Path:"timezone", PathText:"Timezone", Default:"Asia/Shanghai", IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"Timezone (follow IANA Time Zone Database) -- see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones --"},
meta.OptionsMeta{Title:"Trusted domains", Desc:"", Type:"[]interface {}", Options:[]string(nil), Env:"ATK_TRUSTED_DOMAINS", Path:"trusted_domains", PathText:"Trusted domains", Default:[]interface {}{}, IsRoot:true, HasChild:false, AllowsSet:true, CommentRaw:"Trusted domains -- e.g. [\"https://artalk.example.com:23366\"] add url of your site her --"},
}
+18
View File
@@ -73,6 +73,10 @@ type DBConf struct {
Charset string `koanf:"charset" json:"charset"`
SSL bool `koanf:"ssl" json:"ssl"`
PrepareStmt *bool `koanf:"prepare_stmt" json:"prepare_stmt"`
ServerCaPath string `koanf:"server_ca_path" json:"server_ca_path"` // /path/to/server-ca.pem
ClientCertPath string `koanf:"client_cert_path" json:"client_cert_path"` // /path/to/client-cert.pem
ClientKeyPath string `koanf:"client_key_path" json:"client_key_path"` // /path/to/client-key.pem
}
type CacheConf struct {
@@ -454,4 +458,18 @@ type AuthConf struct {
ClientSecret string `koanf:"client_secret" json:"client_secret"`
Domain string `koanf:"domain" json:"domain"`
} `koanf:"auth0" json:"auth0"`
// Token exchange — accept an access token from an external OIDC IdP
// (verified via that IdP's /userinfo) and issue an Artalk JWT for the
// matching user. Lets a surrounding application that already runs OIDC
// (e.g. an Auth0 SPA) drive Artalk sign-in without showing Artalk's
// own popup login.
SSO AuthSSOConf `koanf:"sso" json:"sso"`
}
type AuthSSOConf struct {
Enabled bool `koanf:"enabled" json:"enabled"`
// Issuer is the OIDC issuer URL — e.g. "tenant.auth0.com" or
// "https://tenant.auth0.com". /userinfo is called against this host.
Issuer string `koanf:"issuer" json:"issuer"`
}
+145 -11
View File
@@ -8,6 +8,7 @@ import (
"path"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
@@ -69,7 +70,7 @@ func main() {
case "json":
result = getJSON(metas)
case "markdown":
result = getMarkdown(metas)
result = getMarkdown(metas, outputFile)
default:
panic("invalid output format")
}
@@ -101,13 +102,45 @@ func main() {
}
}
func formatMap(m map[string]string) string {
// To ensure consistent output, we'll sort the map keys.
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
str := fmt.Sprintf("%T{\n", m)
for _, k := range keys {
str += fmt.Sprintf("\t%#v: %#v,\n", k, m[k])
}
str += "}"
return str
}
func formatOptionsMetaSlice(s []meta.OptionsMeta) string {
// To ensure consistent output, we'll sort by meta.OptionsMeta.Env
sorted := make([]meta.OptionsMeta, len(s))
copy(sorted, s)
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].Env < sorted[j].Env
})
str := fmt.Sprintf("%T{\n", s)
for _, item := range sorted {
str += fmt.Sprintf("\t%#v,\n", item)
}
str += "}"
return str
}
func getGo(metas []meta.OptionsMeta, mapping map[string]string) string {
str := "// Code generated by go run internal/config/meta/gen/main.go. DO NOT EDIT.\n\n"
str += "package " + pkgName + "\n\nimport \"github.com/artalkjs/artalk/v2/internal/config/meta\"\n\n"
str += "// Cache result of `meta.GetEnvPathMap(config.Config{}, config.Template(\"" + locale + "\"))`\n"
str += fmt.Sprintf("var EnvPathMapCache = %#v\n\n", mapping)
str += "var EnvPathMapCache = " + formatMap(mapping) + "\n\n"
str += "// Cache result of `meta.GetOptionsMetaData(config.Template(\"" + locale + "\"))`\n"
str += fmt.Sprintf("var OptionsMetaCache = %#v\n", metas)
str += "var OptionsMetaCache = " + formatOptionsMetaSlice(metas) + "\n"
return str
}
@@ -119,14 +152,21 @@ func getJSON(metas []meta.OptionsMeta) string {
return string(str)
}
func getMarkdown(metas []meta.OptionsMeta) string {
func getMarkdown(metas []meta.OptionsMeta, templateFile string) string {
generalTableTitle, tableFields := "通用配置", []string{"环境变量", "默认值", "描述", "路径"}
if templateFile != "" {
if _, err := os.Stat(templateFile); err == nil {
_, _, _, generalTableTitle, tableFields = parseMarkdownEnv(templateFile)
}
}
md := ""
// Get root nodes
rootNodes := []meta.OptionsMeta{
{
Title: "通用配置",
Desc: "通用配置项",
Title: generalTableTitle,
Desc: generalTableTitle,
Path: "general",
IsRoot: true,
},
@@ -137,7 +177,7 @@ func getMarkdown(metas []meta.OptionsMeta) string {
printGroup := func(root meta.OptionsMeta, items []meta.OptionsMeta) {
md += fmt.Sprintf("## %s\n\n", root.Title)
md += fmt.Sprintln("| 环境变量 | 默认值 | 描述 | 路径 |")
md += "| " + strings.Join(tableFields, " | ") + " |\n"
md += fmt.Sprintln("| --- | --- | --- | --- |")
for _, m := range items {
if m.Title == "Enabled" {
@@ -169,6 +209,103 @@ func getMarkdown(metas []meta.OptionsMeta) string {
return strings.TrimSpace(md)
}
var start = "<!-- env-variables -->"
var end = "<!-- /env-variables -->"
// parseMarkdownEnv extracts the env.md to be:
// - header: content before the env variables section
// - body: the content between start and end markers
// - firstTableTitle: the first title (## ...) after the start marker
// - tableFields: the first table header row after the first table title
// - footer: content after the env variables section
//
// For example, given a snippet from env.md:
// ```md
// ...header...
// <!-- env-variables -->
// ## 通用配置
//
// | 环境变量 | 默认值 | 描述 | 路径 |
// | --- | --- | --- | --- |
// | **ATK_ADMIN_USERS** | `[]` | 管理员账户 | admin_users (管理员账户) |
// ...more tables...
// <!-- /env-variables -->
// ...footer...
// ```
// The function should return:
// header: "...header...\n"
// body: "\n## 通用配置\n\n| 环境变量 | 默认值 | 描述 | 路径 |\n| --- | --- | --- | --- |\n| **ATK_ADMIN_USERS** | `[]` | 管理员账户 | admin_users (管理员账户) |\n...more tables...\n"
// footer: "\n...footer..."
// firstTableTitle: "通用配置"
// tableFields: ["环境变量", "默认值", "描述", "路径"]
func parseMarkdownEnv(file string) (header, body, footer, firstTableTitle string, tableFields []string) {
contentBytes, err := os.ReadFile(file)
if err != nil {
panic(err)
}
content := string(contentBytes)
startIdx := strings.Index(content, start)
endIdx := strings.Index(content, end)
if startIdx == -1 || endIdx == -1 || endIdx < startIdx {
panic(fmt.Sprintf("%s markers not found or invalid", start))
}
header = content[:startIdx]
body = content[startIdx+len(start) : endIdx]
footer = content[endIdx+len(end):]
// Find the first title (## ...) after the start marker
afterStart := content[startIdx+len(start):]
titleIdx := strings.Index(afterStart, "## ")
if titleIdx == -1 {
firstTableTitle = ""
} else {
// Find the end of the line
titleLineEnd := strings.Index(afterStart[titleIdx:], "\n")
if titleLineEnd == -1 {
firstTableTitle = strings.TrimSpace(afterStart[titleIdx+3:])
} else {
firstTableTitle = strings.TrimSpace(afterStart[titleIdx+3 : titleIdx+titleLineEnd])
}
}
// Find the first table header after the general table title
tableHeaderIdx := -1
if titleIdx != -1 {
afterTitle := afterStart[titleIdx:]
tableHeaderIdx = strings.Index(afterTitle, "|")
if tableHeaderIdx != -1 {
// The first line starting with '|' is the table header
tableHeaderLineEnd := strings.Index(afterTitle[tableHeaderIdx:], "\n")
var tableHeaderLine string
if tableHeaderLineEnd == -1 {
tableHeaderLine = afterTitle[tableHeaderIdx:]
} else {
tableHeaderLine = afterTitle[tableHeaderIdx : tableHeaderIdx+tableHeaderLineEnd]
}
// Split and trim fields
fields := strings.Split(tableHeaderLine, "|")
for _, f := range fields {
f = strings.TrimSpace(f)
if f != "" {
tableFields = append(tableFields, f)
}
}
}
}
// Remove empty fields at the start/end (from leading/trailing '|')
if len(tableFields) > 0 && tableFields[0] == "" {
tableFields = tableFields[1:]
}
if len(tableFields) > 0 && tableFields[len(tableFields)-1] == "" {
tableFields = tableFields[:len(tableFields)-1]
}
body = strings.Trim(body, "\n")
return
}
func writeMarkdown(file string, text string) {
// read file content
content, err := os.ReadFile(file)
@@ -176,13 +313,10 @@ func writeMarkdown(file string, text string) {
panic(err)
}
// find `<!-- env-variables --><!-- /env-variables -->`
start := "<!-- env-variables -->"
end := "<!-- /env-variables -->"
re := regexp.MustCompile(start + `(?s:(.*?))` + end)
matches := re.FindAllStringSubmatch(string(content), -1)
if len(matches) == 0 {
panic("<!-- env-variables --> not found")
panic(fmt.Sprintf("%s not found", start))
}
// replace
+12 -1
View File
@@ -9,13 +9,24 @@ import (
// The version of Artalk
//
// Which is automatically set by the CI release workflow
const Version = "v2.9.1"
var Version = "v2.10.0"
// buildCommitHash is optionally set by build scripts when the VCS metadata is
// not available in the build context (for example, inside a Docker build).
var buildCommitHash string
// The commit hash from which the binary was built (optional)
//
// This value is set by the build script:
// `go build -ldflags "-X 'github.com/artalkjs/artalk/v2/internal/config.CommitHash=$(git rev-parse --short HEAD)'"`
func CommitHash() string {
if buildCommitHash != "" {
if len(buildCommitHash) >= 7 {
return buildCommitHash[:7]
}
return buildCommitHash
}
info, ok := debug.ReadBuildInfo()
if !ok {
return ""
+41 -8
View File
@@ -58,6 +58,42 @@ func (app *App) registerDefaultHooks() {
var mutex = sync.Mutex{}
type timezoneInitializer struct {
once sync.Once
name string
err error
}
func (i *timezoneInitializer) init(timezone string, apply func(*time.Location)) error {
if timezone == "" {
timezone = "Local"
}
i.once.Do(func() {
i.name = timezone
if timezone == "Local" {
return
}
local, err := time.LoadLocation(timezone)
if err != nil {
i.err = fmt.Errorf("timezone load error: %w (please check config or system env)", err)
return
}
apply(local)
})
if i.err != nil {
return i.err
}
if i.name != timezone {
return fmt.Errorf("timezone is already initialized as %q; restart the process to change it to %q", i.name, timezone)
}
return nil
}
var processTimezone timezoneInitializer
// Bootstrap implements App.
func (app *App) Bootstrap() error {
mutex.Lock()
@@ -67,14 +103,11 @@ func (app *App) Bootstrap() error {
return fmt.Errorf("app.conf cannot be nil while bootstrap")
}
// 时区设置
timezone := app.Conf().TimeZone
if timezone != "" {
if local, err := time.LoadLocation(timezone); err == nil {
time.Local = local
} else {
return fmt.Errorf("timezone load error: %w (please check config or system env)", err)
}
// time.Local is process-global and must not be mutated after services start.
if err := processTimezone.init(app.Conf().TimeZone, func(local *time.Location) {
time.Local = local
}); err != nil {
return err
}
// i18n
+42
View File
@@ -1,10 +1,14 @@
package core
import (
"sync"
"sync/atomic"
"testing"
"time"
"github.com/artalkjs/artalk/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type MockService struct{}
@@ -23,6 +27,44 @@ func TestNewApp(t *testing.T) {
assert.NotNil(t, app.service)
}
func TestTimezoneInitializer(t *testing.T) {
var initializer timezoneInitializer
var applyCount atomic.Int32
apply := func(location *time.Location) {
assert.Equal(t, time.UTC, location)
applyCount.Add(1)
}
const callers = 32
errs := make(chan error, callers)
var wg sync.WaitGroup
for range callers {
wg.Add(1)
go func() {
defer wg.Done()
errs <- initializer.init("UTC", apply)
}()
}
wg.Wait()
close(errs)
for err := range errs {
require.NoError(t, err)
}
assert.Equal(t, int32(1), applyCount.Load())
err := initializer.init("Asia/Shanghai", apply)
assert.ErrorContains(t, err, "restart the process")
assert.Equal(t, int32(1), applyCount.Load())
var invalid timezoneInitializer
err = invalid.init("Invalid/Timezone", apply)
assert.ErrorContains(t, err, "timezone load error")
var local timezoneInitializer
require.NoError(t, local.init("Local", apply))
assert.ErrorContains(t, local.init("UTC", apply), "restart the process")
}
func TestAppBootstrap(t *testing.T) {
conf := &config.Config{
Cache: config.CacheConf{
+1 -1
View File
@@ -36,7 +36,7 @@ func NewDB(conf config.DBConf) (*gorm.DB, error) {
case config.TypeSQLite:
return OpenSQLite(dsn, gormConfig)
case config.TypeMySql:
return OpenMySql(dsn, gormConfig)
return OpenMySql(dsn, gormConfig, &conf)
case config.TypePostgreSQL:
return OpenPostgreSQL(dsn, gormConfig)
case config.TypeMSSQL:
+5 -1
View File
@@ -25,6 +25,10 @@ func getDsnByConf(conf config.DBConf) string {
)
case config.TypeMySql:
tlsMode := lo.If(conf.SSL, "true").Else("false")
if conf.ServerCaPath != "" && conf.ClientCertPath != "" && conf.ClientKeyPath != "" {
tlsMode = mysqlCloudSQLTLSConfigName
}
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local&tls=%s",
conf.User,
conf.Password,
@@ -32,7 +36,7 @@ func getDsnByConf(conf config.DBConf) string {
conf.Port,
conf.Name,
conf.Charset,
lo.If(conf.SSL, "true").Else("false"),
tlsMode,
)
case config.TypeMSSQL:
+16
View File
@@ -47,6 +47,22 @@ func TestGetDsnByConf(t *testing.T) {
},
expected: "user:password@tcp(localhost:3306)/dbname?charset=utf8&parseTime=True&loc=Local&tls=false",
},
{
name: "MySQL Cloud SQL TLS configuration",
conf: config.DBConf{
Type: config.TypeMySql,
Host: "db.example.internal",
User: "user",
Password: "password",
Name: "dbname",
Port: 3306,
Charset: "utf8",
ServerCaPath: "server-ca.pem",
ClientCertPath: "client-cert.pem",
ClientKeyPath: "client-key.pem",
},
expected: "user:password@tcp(db.example.internal:3306)/dbname?charset=utf8&parseTime=True&loc=Local&tls=cloudsql",
},
{
name: "SQL Server configuration",
conf: config.DBConf{
+55 -2
View File
@@ -1,17 +1,24 @@
package db
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"path/filepath"
"github.com/artalkjs/artalk/v2/internal/config"
"github.com/artalkjs/artalk/v2/internal/utils"
gomysql "github.com/go-sql-driver/mysql"
"github.com/libtnb/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
)
const mysqlCloudSQLTLSConfigName = "cloudsql"
func OpenSQLite(filename string, gormConfig *gorm.Config) (*gorm.DB, error) {
if filename == "" {
return nil, fmt.Errorf("please set `db.file` option in config file to specify a sqlite database path")
@@ -22,10 +29,56 @@ func OpenSQLite(filename string, gormConfig *gorm.Config) (*gorm.DB, error) {
return gorm.Open(sqlite.Open(filename), gormConfig)
}
func OpenMySql(dsn string, gormConfig *gorm.Config) (*gorm.DB, error) {
func OpenMySql(dsn string, gormConfig *gorm.Config, dbConf *config.DBConf) (*gorm.DB, error) {
tlsConfig, configured, err := loadMySQLTLSConfig(dbConf)
if err != nil {
return nil, err
}
if configured {
if err := gomysql.RegisterTLSConfig(mysqlCloudSQLTLSConfigName, tlsConfig); err != nil {
return nil, fmt.Errorf("failed to register MySQL TLS config: %w", err)
}
}
return gorm.Open(mysql.Open(dsn), gormConfig)
}
func loadMySQLTLSConfig(dbConf *config.DBConf) (*tls.Config, bool, error) {
paths := []string{dbConf.ServerCaPath, dbConf.ClientCertPath, dbConf.ClientKeyPath}
configuredPaths := 0
for _, path := range paths {
if path != "" {
configuredPaths++
}
}
if configuredPaths == 0 {
return nil, false, nil
}
if configuredPaths != len(paths) {
return nil, false, fmt.Errorf("server CA, client certificate, and client key must be configured together")
}
serverCertPool := x509.NewCertPool()
pem, err := os.ReadFile(dbConf.ServerCaPath)
if err != nil {
return nil, false, fmt.Errorf("failed to read server CA file: %w", err)
}
if ok := serverCertPool.AppendCertsFromPEM(pem); !ok {
return nil, false, fmt.Errorf("unable to append root cert to pool")
}
clientCert, err := tls.LoadX509KeyPair(dbConf.ClientCertPath, dbConf.ClientKeyPath)
if err != nil {
return nil, false, fmt.Errorf("failed to load client certificate/key: %w", err)
}
return &tls.Config{
RootCAs: serverCertPool,
Certificates: []tls.Certificate{clientCert},
ServerName: dbConf.Host,
MinVersion: tls.VersionTLS12,
}, true, nil
}
func OpenPostgreSQL(dsn string, gormConfig *gorm.Config) (*gorm.DB, error) {
return gorm.Open(postgres.New(postgres.Config{
DSN: dsn,
+121
View File
@@ -0,0 +1,121 @@
package db
import (
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"os"
"path/filepath"
"testing"
"time"
"github.com/artalkjs/artalk/v2/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadMySQLTLSConfig(t *testing.T) {
certDir := t.TempDir()
caCert, caKey := createTestCertificate(t, nil, nil, x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test CA"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
IsCA: true,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
})
serverCert, _ := createTestCertificate(t, caCert, caKey, x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: "db.example.internal"},
DNSNames: []string{"db.example.internal"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
})
clientCert, clientKey := createTestCertificate(t, caCert, caKey, x509.Certificate{
SerialNumber: big.NewInt(3),
Subject: pkix.Name{CommonName: "client"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
})
caPath := writeTestCertificate(t, certDir, "server-ca.pem", caCert.Raw)
clientCertPath := writeTestCertificate(t, certDir, "client-cert.pem", clientCert.Raw)
clientKeyPath := writeTestPrivateKey(t, certDir, "client-key.pem", clientKey)
tlsConfig, configured, err := loadMySQLTLSConfig(&config.DBConf{
Host: "db.example.internal",
ServerCaPath: caPath,
ClientCertPath: clientCertPath,
ClientKeyPath: clientKeyPath,
})
require.NoError(t, err)
assert.True(t, configured)
assert.False(t, tlsConfig.InsecureSkipVerify)
assert.Equal(t, "db.example.internal", tlsConfig.ServerName)
assert.Equal(t, uint16(tls.VersionTLS12), tlsConfig.MinVersion)
assert.Len(t, tlsConfig.Certificates, 1)
_, err = serverCert.Verify(x509.VerifyOptions{
DNSName: tlsConfig.ServerName,
Roots: tlsConfig.RootCAs,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
})
require.NoError(t, err)
_, err = serverCert.Verify(x509.VerifyOptions{
DNSName: "attacker.example.internal",
Roots: tlsConfig.RootCAs,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
})
assert.Error(t, err, "A certificate for a different host must be rejected")
}
func TestLoadMySQLTLSConfigRequiresCompleteCertificateSet(t *testing.T) {
_, _, err := loadMySQLTLSConfig(&config.DBConf{ServerCaPath: "server-ca.pem"})
assert.EqualError(t, err, "server CA, client certificate, and client key must be configured together")
}
func createTestCertificate(
t *testing.T,
parent *x509.Certificate,
parentKey ed25519.PrivateKey,
template x509.Certificate,
) (*x509.Certificate, ed25519.PrivateKey) {
t.Helper()
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
if parent == nil {
parent = &template
parentKey = privateKey
}
der, err := x509.CreateCertificate(rand.Reader, &template, parent, publicKey, parentKey)
require.NoError(t, err)
cert, err := x509.ParseCertificate(der)
require.NoError(t, err)
return cert, privateKey
}
func writeTestCertificate(t *testing.T, dir string, name string, der []byte) string {
t.Helper()
path := filepath.Join(dir, name)
require.NoError(t, os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600))
return path
}
func writeTestPrivateKey(t *testing.T, dir string, name string, key ed25519.PrivateKey) string {
t.Helper()
der, err := x509.MarshalPKCS8PrivateKey(key)
require.NoError(t, err)
path := filepath.Join(dir, name)
require.NoError(t, os.WriteFile(path, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), 0o600))
return path
}
+31 -13
View File
@@ -18,7 +18,7 @@ func TestQueue(t *testing.T) {
}
t.Run("Send", func(t *testing.T) {
sendSuccessHookTriggered := false
sendSuccess := make(chan Email, 1)
q := NewQueue(EmailConf{
EmailConf: config.EmailConf{},
@@ -26,37 +26,53 @@ func TestQueue(t *testing.T) {
SendResult: true,
},
OnSendSuccess: func(email *Email) {
assert.Equal(t, testEmail, *email)
sendSuccessHookTriggered = true
sendSuccess <- *email
},
})
defer q.Close()
q.Push(&testEmail)
time.Sleep(100 * time.Millisecond)
assert.True(t, sendSuccessHookTriggered, "Send success hook should be triggered")
select {
case email := <-sendSuccess:
assert.Equal(t, testEmail, email)
case <-time.After(time.Second):
t.Fatal("Send success hook should be triggered")
}
})
t.Run("Send failed", func(t *testing.T) {
sendSuccessHookTriggered := false
sendAttempted := make(chan Email, 1)
sendSuccess := make(chan Email, 1)
q := NewQueue(EmailConf{
EmailConf: config.EmailConf{},
Sender: &mockSender{
SendResult: false,
OnSend: func(email *Email) {
sendAttempted <- *email
},
},
OnSendSuccess: func(email *Email) {
assert.Equal(t, testEmail, email)
sendSuccessHookTriggered = true
sendSuccess <- *email
},
})
defer q.Close()
q.Push(&testEmail)
time.Sleep(100 * time.Millisecond)
select {
case email := <-sendAttempted:
assert.Equal(t, testEmail, email)
case <-time.After(time.Second):
t.Fatal("Email send should be attempted")
}
assert.False(t, sendSuccessHookTriggered, "Send success hook should not be triggered")
select {
case <-sendSuccess:
t.Fatal("Send success hook should not be triggered")
default:
}
})
t.Run("Close", func(t *testing.T) {
@@ -69,8 +85,6 @@ func TestQueue(t *testing.T) {
q.Close()
time.Sleep(100 * time.Millisecond)
assert.True(t, q.closed, "Queue should be closed")
})
}
@@ -81,10 +95,14 @@ func TestQueue(t *testing.T) {
type mockSender struct {
SendResult bool
OnSend func(email *Email)
}
var _ Sender = (*mockSender)(nil)
func (s *mockSender) Send(email *Email) bool {
if s.OnSend != nil {
s.OnSend(email)
}
return s.SendResult
}
+18
View File
@@ -2,10 +2,15 @@ package email
import (
"bytes"
"net/mail"
"strings"
"github.com/artalkjs/artalk/v2/internal/utils"
"gopkg.in/gomail.v2"
)
const fallbackMessageIDDomain = "artalk.local"
func getCookedEmail(email *Email) *gomail.Message {
m := gomail.NewMessage()
@@ -17,6 +22,8 @@ func getCookedEmail(email *Email) *gomail.Message {
//m.SetAddressHeader("Cc", "dan@example.com", "Dan")
// 主题
m.SetHeader("Subject", email.Subject)
// 唯一邮件标识
m.SetHeader("Message-ID", newMessageID(email.FromAddr))
// 内容
m.SetBody("text/html", email.Body)
// 附件
@@ -25,6 +32,17 @@ func getCookedEmail(email *Email) *gomail.Message {
return m
}
func newMessageID(fromAddr string) string {
domain := fallbackMessageIDDomain
if addr, err := mail.ParseAddress(fromAddr); err == nil {
if i := strings.LastIndexByte(addr.Address, '@'); i >= 0 && i < len(addr.Address)-1 {
domain = addr.Address[i+1:]
}
}
return "<" + utils.RandomString(32) + "@" + domain + ">"
}
func getEmailMineTxt(email *Email) string {
emailBuffer := bytes.NewBuffer([]byte{})
getCookedEmail(email).WriteTo(emailBuffer)
Loaded 100 of 198 files, more files were not shown because too many files have changed in this diff. Show more